summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--.gitlab-ci.yml8
-rw-r--r--.rubocop.yml4
-rw-r--r--CHANGELOG117
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile213
-rw-r--r--Gemfile.lock564
-rw-r--r--Procfile2
-rw-r--r--README.md4
-rw-r--r--VERSION2
-rwxr-xr-xapp/assets/fonts/OFL.txt92
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Black.ttfbin0 -> 148368 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BlackItalic.ttfbin0 -> 116360 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Bold.ttfbin0 -> 291424 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-BoldItalic.ttfbin0 -> 116192 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-ExtraLight.ttfbin0 -> 150528 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-ExtraLightItalic.ttfbin0 -> 117140 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Italic.ttfbin0 -> 117328 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Light.ttfbin0 -> 293220 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-LightItalic.ttfbin0 -> 116960 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Regular.ttfbin0 -> 293956 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-Semibold.ttfbin0 -> 292404 bytes
-rwxr-xr-xapp/assets/fonts/SourceSansPro-SemiboldItalic.ttfbin0 -> 116424 bytes
-rw-r--r--app/assets/images/ci/arch.jpgbin0 -> 25222 bytes
-rw-r--r--app/assets/images/ci/favicon.icobin0 -> 5430 bytes
-rw-r--r--app/assets/images/ci/loader.gifbin0 -> 4405 bytes
-rw-r--r--app/assets/images/ci/no_avatar.pngbin0 -> 1337 bytes
-rw-r--r--app/assets/images/ci/rails.pngbin0 -> 6646 bytes
-rw-r--r--app/assets/images/ci/service_sample.pngbin0 -> 76024 bytes
-rw-r--r--app/assets/javascripts/activities.js.coffee4
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js.coffee67
-rw-r--r--app/assets/javascripts/ci/Chart.min.js39
-rw-r--r--app/assets/javascripts/ci/application.js.coffee40
-rw-r--r--app/assets/javascripts/ci/build.coffee41
-rw-r--r--app/assets/javascripts/ci/projects.js.coffee6
-rw-r--r--app/assets/javascripts/dropzone_input.js.coffee1
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee9
-rw-r--r--app/assets/javascripts/merge_request_tabs.js.coffee7
-rw-r--r--app/assets/javascripts/merge_request_widget.js.coffee11
-rw-r--r--app/assets/javascripts/notes.js.coffee20
-rw-r--r--app/assets/javascripts/project.js.coffee16
-rw-r--r--app/assets/javascripts/syntax_highlight.coffee13
-rw-r--r--app/assets/javascripts/tree.js.coffee5
-rw-r--r--app/assets/javascripts/zen_mode.js.coffee2
-rw-r--r--app/assets/stylesheets/application.scss7
-rw-r--r--app/assets/stylesheets/base/fonts.scss25
-rw-r--r--app/assets/stylesheets/base/gl_bootstrap.scss26
-rw-r--r--app/assets/stylesheets/base/gl_variables.scss57
-rw-r--r--app/assets/stylesheets/base/layout.scss1
-rw-r--r--app/assets/stylesheets/base/mixins.scss140
-rw-r--r--app/assets/stylesheets/base/variables.scss30
-rw-r--r--app/assets/stylesheets/ci/builds.scss76
-rw-r--r--app/assets/stylesheets/ci/lint.scss10
-rw-r--r--app/assets/stylesheets/ci/projects.scss59
-rw-r--r--app/assets/stylesheets/ci/runners.scss36
-rw-r--r--app/assets/stylesheets/ci/status.scss37
-rw-r--r--app/assets/stylesheets/ci/xterm.scss906
-rw-r--r--app/assets/stylesheets/generic/avatar.scss9
-rw-r--r--app/assets/stylesheets/generic/blocks.scss49
-rw-r--r--app/assets/stylesheets/generic/buttons.scss156
-rw-r--r--app/assets/stylesheets/generic/callout.scss45
-rw-r--r--app/assets/stylesheets/generic/common.scss51
-rw-r--r--app/assets/stylesheets/generic/files.scss37
-rw-r--r--app/assets/stylesheets/generic/filters.scss25
-rw-r--r--app/assets/stylesheets/generic/header.scss32
-rw-r--r--app/assets/stylesheets/generic/issue_box.scss9
-rw-r--r--app/assets/stylesheets/generic/lists.scss36
-rw-r--r--app/assets/stylesheets/generic/markdown_area.scss11
-rw-r--r--app/assets/stylesheets/generic/mobile.scss17
-rw-r--r--app/assets/stylesheets/generic/pagination.scss34
-rw-r--r--app/assets/stylesheets/generic/selects.scss8
-rw-r--r--app/assets/stylesheets/generic/sidebar.scss80
-rw-r--r--app/assets/stylesheets/generic/timeline.scss140
-rw-r--r--app/assets/stylesheets/generic/typography.scss41
-rw-r--r--app/assets/stylesheets/generic/zen.scss4
-rw-r--r--app/assets/stylesheets/highlight/white.scss15
-rw-r--r--app/assets/stylesheets/pages/commit.scss57
-rw-r--r--app/assets/stylesheets/pages/commits.scss6
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss16
-rw-r--r--app/assets/stylesheets/pages/editor.scss18
-rw-r--r--app/assets/stylesheets/pages/events.scss86
-rw-r--r--app/assets/stylesheets/pages/groups.scss6
-rw-r--r--app/assets/stylesheets/pages/issuable.scss46
-rw-r--r--app/assets/stylesheets/pages/issues.scss14
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss39
-rw-r--r--app/assets/stylesheets/pages/milestone.scss4
-rw-r--r--app/assets/stylesheets/pages/note_form.scss18
-rw-r--r--app/assets/stylesheets/pages/notes.scss31
-rw-r--r--app/assets/stylesheets/pages/projects.scss336
-rw-r--r--app/assets/stylesheets/pages/tree.scss56
-rw-r--r--app/assets/stylesheets/themes/gitlab-theme.scss14
-rw-r--r--app/controllers/admin/application_settings_controller.rb1
-rw-r--r--app/controllers/admin/labels_controller.rb58
-rw-r--r--app/controllers/admin/users_controller.rb8
-rw-r--r--app/controllers/application_controller.rb17
-rw-r--r--app/controllers/autocomplete_controller.rb1
-rw-r--r--app/controllers/ci/admin/application_controller.rb10
-rw-r--r--app/controllers/ci/admin/application_settings_controller.rb31
-rw-r--r--app/controllers/ci/admin/builds_controller.rb18
-rw-r--r--app/controllers/ci/admin/events_controller.rb9
-rw-r--r--app/controllers/ci/admin/projects_controller.rb19
-rw-r--r--app/controllers/ci/admin/runner_projects_controller.rb34
-rw-r--r--app/controllers/ci/admin/runners_controller.rb72
-rw-r--r--app/controllers/ci/application_controller.rb74
-rw-r--r--app/controllers/ci/builds_controller.rb52
-rw-r--r--app/controllers/ci/commits_controller.rb32
-rw-r--r--app/controllers/ci/events_controller.rb21
-rw-r--r--app/controllers/ci/lints_controller.rb26
-rw-r--r--app/controllers/ci/projects_controller.rb56
-rw-r--r--app/controllers/ci/runner_projects_controller.rb36
-rw-r--r--app/controllers/ci/services_controller.rb59
-rw-r--r--app/controllers/dashboard/projects_controller.rb16
-rw-r--r--app/controllers/dashboard/snippets_controller.rb10
-rw-r--r--app/controllers/dashboard_controller.rb28
-rw-r--r--app/controllers/explore/application_controller.rb2
-rw-r--r--app/controllers/explore/groups_controller.rb3
-rw-r--r--app/controllers/explore/projects_controller.rb3
-rw-r--r--app/controllers/explore/snippets_controller.rb6
-rw-r--r--app/controllers/groups_controller.rb6
-rw-r--r--app/controllers/help_controller.rb7
-rw-r--r--app/controllers/import/fogbugz_controller.rb104
-rw-r--r--app/controllers/invites_controller.rb4
-rw-r--r--app/controllers/namespaces_controller.rb2
-rw-r--r--app/controllers/oauth/applications_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb4
-rw-r--r--app/controllers/passwords_controller.rb61
-rw-r--r--app/controllers/profiles/preferences_controller.rb1
-rw-r--r--app/controllers/profiles/two_factor_auths_controller.rb2
-rw-r--r--app/controllers/projects/application_controller.rb10
-rw-r--r--app/controllers/projects/blob_controller.rb28
-rw-r--r--app/controllers/projects/builds_controller.rb25
-rw-r--r--app/controllers/projects/ci_settings_controller.rb36
-rw-r--r--app/controllers/projects/ci_web_hooks_controller.rb45
-rw-r--r--app/controllers/projects/commit_controller.rb9
-rw-r--r--app/controllers/projects/compare_controller.rb10
-rw-r--r--app/controllers/projects/forks_controller.rb12
-rw-r--r--app/controllers/projects/graphs_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb11
-rw-r--r--app/controllers/projects/milestones_controller.rb7
-rw-r--r--app/controllers/projects/project_members_controller.rb2
-rw-r--r--app/controllers/projects/raw_controller.rb5
-rw-r--r--app/controllers/projects/runners_controller.rb65
-rw-r--r--app/controllers/projects/services_controller.rb6
-rw-r--r--app/controllers/projects/triggers_controller.rb35
-rw-r--r--app/controllers/projects/variables_controller.rb25
-rw-r--r--app/controllers/projects/wikis_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb10
-rw-r--r--app/controllers/root_controller.rb11
-rw-r--r--app/controllers/sessions_controller.rb4
-rw-r--r--app/controllers/snippets_controller.rb8
-rw-r--r--app/finders/issuable_finder.rb2
-rw-r--r--app/finders/trending_projects_finder.rb17
-rw-r--r--app/helpers/application_helper.rb14
-rw-r--r--app/helpers/auth_helper.rb8
-rw-r--r--app/helpers/builds_helper.rb13
-rw-r--r--app/helpers/ci/gitlab_helper.rb36
-rw-r--r--app/helpers/ci/projects_helper.rb36
-rw-r--r--app/helpers/ci_status_helper.rb45
-rw-r--r--app/helpers/commits_helper.rb2
-rw-r--r--app/helpers/diff_helper.rb23
-rw-r--r--app/helpers/events_helper.rb22
-rw-r--r--app/helpers/gitlab_markdown_helper.rb4
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/graph_helper.rb5
-rw-r--r--app/helpers/groups_helper.rb11
-rw-r--r--app/helpers/merge_requests_helper.rb13
-rw-r--r--app/helpers/notifications_helper.rb45
-rw-r--r--app/helpers/page_layout_helper.rb24
-rw-r--r--app/helpers/preferences_helper.rb11
-rw-r--r--app/helpers/projects_helper.rb42
-rw-r--r--app/helpers/runners_helper.rb20
-rw-r--r--app/helpers/time_helper.rb27
-rw-r--r--app/helpers/triggers_helper.rb5
-rw-r--r--app/helpers/version_check_helper.rb2
-rw-r--r--app/helpers/wiki_helper.rb24
-rw-r--r--app/mailers/ci/emails/builds.rb17
-rw-r--r--app/mailers/ci/notify.rb46
-rw-r--r--app/mailers/emails/notes.rb6
-rw-r--r--app/mailers/emails/projects.rb3
-rw-r--r--app/mailers/notify.rb8
-rw-r--r--app/models/ability.rb1
-rw-r--r--app/models/abuse_report.rb16
-rw-r--r--app/models/application_setting.rb2
-rw-r--r--app/models/ci/application_setting.rb27
-rw-r--r--app/models/ci/build.rb309
-rw-r--r--app/models/ci/commit.rb246
-rw-r--r--app/models/ci/event.rb27
-rw-r--r--app/models/ci/project.rb215
-rw-r--r--app/models/ci/project_status.rb35
-rw-r--r--app/models/ci/runner.rb84
-rw-r--r--app/models/ci/runner_project.rb21
-rw-r--r--app/models/ci/service.rb105
-rw-r--r--app/models/ci/trigger.rb39
-rw-r--r--app/models/ci/trigger_request.rb23
-rw-r--r--app/models/ci/variable.rb25
-rw-r--r--app/models/ci/web_hook.rb44
-rw-r--r--app/models/concerns/issuable.rb6
-rw-r--r--app/models/event.rb12
-rw-r--r--app/models/hooks/web_hook.rb2
-rw-r--r--app/models/issue.rb27
-rw-r--r--app/models/label.rb8
-rw-r--r--app/models/merge_request.rb15
-rw-r--r--app/models/merge_request_diff.rb12
-rw-r--r--app/models/milestone.rb2
-rw-r--r--app/models/namespace.rb4
-rw-r--r--app/models/note.rb3
-rw-r--r--app/models/notification.rb21
-rw-r--r--app/models/project.rb85
-rw-r--r--app/models/project_services/buildkite_service.rb8
-rw-r--r--app/models/project_services/ci/hip_chat_message.rb73
-rw-r--r--app/models/project_services/ci/hip_chat_service.rb93
-rw-r--r--app/models/project_services/ci/mail_service.rb84
-rw-r--r--app/models/project_services/ci/slack_message.rb92
-rw-r--r--app/models/project_services/ci/slack_service.rb81
-rw-r--r--app/models/project_services/ci_service.rb37
-rw-r--r--app/models/project_services/drone_ci_service.rb176
-rw-r--r--app/models/project_services/gitlab_ci_service.rb115
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/hipchat_service.rb25
-rw-r--r--app/models/project_services/jira_service.rb2
-rw-r--r--app/models/project_services/slack_service.rb6
-rw-r--r--app/models/project_team.rb4
-rw-r--r--app/models/repository.rb2
-rw-r--r--app/models/sent_notification.rb33
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/user.rb137
-rw-r--r--app/services/ci/create_builds_service.rb27
-rw-r--r--app/services/ci/create_commit_service.rb26
-rw-r--r--app/services/ci/create_trigger_request_service.rb22
-rw-r--r--app/services/ci/event_service.rb31
-rw-r--r--app/services/ci/image_for_build_service.rb31
-rw-r--r--app/services/ci/register_build_service.rb40
-rw-r--r--app/services/ci/test_hook_service.rb7
-rw-r--r--app/services/ci/web_hook_service.rb35
-rw-r--r--app/services/compare_service.rb5
-rw-r--r--app/services/event_create_service.rb4
-rw-r--r--app/services/files/base_service.rb2
-rw-r--r--app/services/files/create_service.rb4
-rw-r--r--app/services/git_push_service.rb12
-rw-r--r--app/services/merge_requests/merge_service.rb4
-rw-r--r--app/services/milestones/destroy_service.rb27
-rw-r--r--app/services/notification_service.rb4
-rw-r--r--app/services/projects/create_service.rb8
-rw-r--r--app/services/projects/download_service.rb43
-rw-r--r--app/services/projects/fork_service.rb8
-rw-r--r--app/services/projects/transfer_service.rb2
-rw-r--r--app/views/admin/application_settings/_form.html.haml5
-rw-r--r--app/views/admin/applications/index.html.haml2
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/labels/_form.html.haml35
-rw-r--r--app/views/admin/labels/_label.html.haml5
-rw-r--r--app/views/admin/labels/destroy.js.haml2
-rw-r--r--app/views/admin/labels/edit.html.haml9
-rw-r--r--app/views/admin/labels/index.html.haml16
-rw-r--r--app/views/admin/labels/new.html.haml7
-rw-r--r--app/views/admin/users/_head.html.haml2
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/ci/admin/application_settings/_form.html.haml24
-rw-r--r--app/views/ci/admin/application_settings/show.html.haml3
-rw-r--r--app/views/ci/admin/builds/_build.html.haml34
-rw-r--r--app/views/ci/admin/builds/index.html.haml28
-rw-r--r--app/views/ci/admin/events/index.html.haml17
-rw-r--r--app/views/ci/admin/projects/_project.html.haml29
-rw-r--r--app/views/ci/admin/projects/index.html.haml15
-rw-r--r--app/views/ci/admin/runner_projects/index.html.haml57
-rw-r--r--app/views/ci/admin/runners/_runner.html.haml48
-rw-r--r--app/views/ci/admin/runners/index.html.haml52
-rw-r--r--app/views/ci/admin/runners/show.html.haml124
-rw-r--r--app/views/ci/admin/runners/update.js.haml2
-rw-r--r--app/views/ci/commits/_commit.html.haml33
-rw-r--r--app/views/ci/errors/show.haml2
-rw-r--r--app/views/ci/events/index.html.haml19
-rw-r--r--app/views/ci/lints/_create.html.haml39
-rw-r--r--app/views/ci/lints/create.js.haml2
-rw-r--r--app/views/ci/lints/show.html.haml25
-rw-r--r--app/views/ci/notify/build_fail_email.html.haml19
-rw-r--r--app/views/ci/notify/build_fail_email.text.erb9
-rw-r--r--app/views/ci/notify/build_success_email.html.haml20
-rw-r--r--app/views/ci/notify/build_success_email.text.erb9
-rw-r--r--app/views/ci/projects/_info.html.haml2
-rw-r--r--app/views/ci/projects/_no_runners.html.haml8
-rw-r--r--app/views/ci/projects/show.html.haml60
-rw-r--r--app/views/ci/services/_form.html.haml57
-rw-r--r--app/views/ci/services/edit.html.haml1
-rw-r--r--app/views/ci/services/index.html.haml22
-rw-r--r--app/views/ci/shared/_guide.html.haml15
-rw-r--r--app/views/ci/shared/_no_runners.html.haml7
-rw-r--r--app/views/ci/user_sessions/new.html.haml8
-rw-r--r--app/views/dashboard/_activities.html.haml6
-rw-r--r--app/views/dashboard/_activity_head.html.haml7
-rw-r--r--app/views/dashboard/_groups_head.html.haml6
-rw-r--r--app/views/dashboard/_projects_head.html.haml8
-rw-r--r--app/views/dashboard/_snippets_head.html.haml7
-rw-r--r--app/views/dashboard/activity.html.haml7
-rw-r--r--app/views/dashboard/groups/index.html.haml11
-rw-r--r--app/views/dashboard/issues.html.haml12
-rw-r--r--app/views/dashboard/merge_requests.html.haml7
-rw-r--r--app/views/dashboard/milestones/_milestone.html.haml24
-rw-r--r--app/views/dashboard/milestones/index.html.haml28
-rw-r--r--app/views/dashboard/projects/_projects.html.haml (renamed from app/views/dashboard/_projects.html.haml)4
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml (renamed from app/views/dashboard/_zero_authorized_projects.html.haml)0
-rw-r--r--app/views/dashboard/projects/index.atom.builder (renamed from app/views/dashboard/show.atom.builder)6
-rw-r--r--app/views/dashboard/projects/index.html.haml (renamed from app/views/dashboard/show.html.haml)5
-rw-r--r--app/views/dashboard/projects/starred.html.haml23
-rw-r--r--app/views/dashboard/snippets/index.html.haml38
-rw-r--r--app/views/devise/passwords/new.html.haml2
-rw-r--r--app/views/devise/sessions/_new_crowd.html.haml9
-rw-r--r--app/views/devise/shared/_signin_box.html.haml10
-rw-r--r--app/views/events/_commit.html.haml2
-rw-r--r--app/views/events/_event.html.haml4
-rw-r--r--app/views/events/_event_last_push.html.haml24
-rw-r--r--app/views/events/event/_common.html.haml5
-rw-r--r--app/views/events/event/_note.html.haml3
-rw-r--r--app/views/explore/_head.html.haml6
-rw-r--r--app/views/explore/groups/index.html.haml13
-rw-r--r--app/views/explore/projects/_filter.html.haml2
-rw-r--r--app/views/explore/projects/index.html.haml10
-rw-r--r--app/views/explore/projects/starred.html.haml14
-rw-r--r--app/views/explore/projects/trending.html.haml20
-rw-r--r--app/views/explore/snippets/index.html.haml18
-rw-r--r--app/views/groups/_projects.html.haml4
-rw-r--r--app/views/groups/edit.html.haml3
-rw-r--r--app/views/groups/group_members/index.html.haml4
-rw-r--r--app/views/groups/issues.html.haml25
-rw-r--r--app/views/groups/merge_requests.html.haml23
-rw-r--r--app/views/groups/milestones/_header_title.html.haml1
-rw-r--r--app/views/groups/milestones/_milestone.html.haml36
-rw-r--r--app/views/groups/milestones/index.html.haml26
-rw-r--r--app/views/groups/milestones/show.html.haml2
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/groups/show.html.haml30
-rw-r--r--app/views/help/index.html.haml39
-rw-r--r--app/views/help/show.html.haml2
-rw-r--r--app/views/import/fogbugz/new.html.haml25
-rw-r--r--app/views/import/fogbugz/new_user_map.html.haml49
-rw-r--r--app/views/import/fogbugz/status.html.haml51
-rw-r--r--app/views/kaminari/gitlab/_first_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_last_page.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_paginator.html.haml11
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/_init_auto_complete.html.haml3
-rw-r--r--app/views/layouts/_page.html.haml10
-rw-r--r--app/views/layouts/ci/_info.html.haml2
-rw-r--r--app/views/layouts/ci/_nav_admin.html.haml33
-rw-r--r--app/views/layouts/ci/_nav_project.html.haml23
-rw-r--r--app/views/layouts/ci/_page.html.haml27
-rw-r--r--app/views/layouts/ci/admin.html.haml11
-rw-r--r--app/views/layouts/ci/application.html.haml11
-rw-r--r--app/views/layouts/ci/notify.html.haml19
-rw-r--r--app/views/layouts/ci/project.html.haml11
-rw-r--r--app/views/layouts/dashboard.html.haml2
-rw-r--r--app/views/layouts/devise.html.haml2
-rw-r--r--app/views/layouts/explore.html.haml5
-rw-r--r--app/views/layouts/group.html.haml4
-rw-r--r--app/views/layouts/group_settings.html.haml1
-rw-r--r--app/views/layouts/header/_default.html.haml9
-rw-r--r--app/views/layouts/header/_public.html.haml4
-rw-r--r--app/views/layouts/nav/_admin.html.haml13
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml46
-rw-r--r--app/views/layouts/nav/_explore.html.haml21
-rw-r--r--app/views/layouts/nav/_group.html.haml61
-rw-r--r--app/views/layouts/nav/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/_project.html.haml19
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml26
-rw-r--r--app/views/layouts/notify.html.haml2
-rw-r--r--app/views/layouts/profile.html.haml2
-rw-r--r--app/views/layouts/project.html.haml9
-rw-r--r--app/views/layouts/project_settings.html.haml1
-rw-r--r--app/views/layouts/snippets.html.haml7
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml2
-rw-r--r--app/views/notify/new_user_email.html.haml2
-rw-r--r--app/views/notify/project_was_moved_email.html.haml2
-rw-r--r--app/views/notify/project_was_moved_email.text.erb2
-rw-r--r--app/views/profiles/accounts/show.html.haml10
-rw-r--r--app/views/profiles/applications.html.haml9
-rw-r--r--app/views/profiles/audit_log.html.haml9
-rw-r--r--app/views/profiles/emails/index.html.haml10
-rw-r--r--app/views/profiles/keys/index.html.haml13
-rw-r--r--app/views/profiles/notifications/show.html.haml10
-rw-r--r--app/views/profiles/passwords/edit.html.haml10
-rw-r--r--app/views/profiles/passwords/new.html.haml2
-rw-r--r--app/views/profiles/preferences/show.html.haml15
-rw-r--r--app/views/profiles/preferences/update.js.erb7
-rw-r--r--app/views/profiles/show.html.haml9
-rw-r--r--app/views/projects/_activity.html.haml3
-rw-r--r--app/views/projects/_home_panel.html.haml38
-rw-r--r--app/views/projects/_last_push.html.haml23
-rw-r--r--app/views/projects/_md_preview.html.haml4
-rw-r--r--app/views/projects/_readme.html.haml2
-rw-r--r--app/views/projects/activity.html.haml3
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_actions.html.haml6
-rw-r--r--app/views/projects/blob/_blob.html.haml2
-rw-r--r--app/views/projects/blob/_header_title.html.haml1
-rw-r--r--app/views/projects/blob/_upload.html.haml28
-rw-r--r--app/views/projects/blob/edit.html.haml4
-rw-r--r--app/views/projects/blob/new.html.haml13
-rw-r--r--app/views/projects/blob/show.html.haml6
-rw-r--r--app/views/projects/branches/_branch.html.haml13
-rw-r--r--app/views/projects/branches/_commit.html.haml7
-rw-r--r--app/views/projects/branches/index.html.haml9
-rw-r--r--app/views/projects/branches/new.html.haml2
-rw-r--r--app/views/projects/builds/_build.html.haml50
-rw-r--r--app/views/projects/builds/show.html.haml159
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml1
-rw-r--r--app/views/projects/buttons/_notifications.html.haml14
-rw-r--r--app/views/projects/buttons/_star.html.haml5
-rw-r--r--app/views/projects/ci_settings/_form.html.haml103
-rw-r--r--app/views/projects/ci_settings/edit.html.haml21
-rw-r--r--app/views/projects/ci_web_hooks/index.html.haml92
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml7
-rw-r--r--app/views/projects/commit/_commit_box.html.haml9
-rw-r--r--app/views/projects/commit/ci.html.haml62
-rw-r--r--app/views/projects/commit/show.html.haml2
-rw-r--r--app/views/projects/commits/_commit.html.haml10
-rw-r--r--app/views/projects/commits/_head.html.haml10
-rw-r--r--app/views/projects/commits/_header_title.html.haml1
-rw-r--r--app/views/projects/commits/show.html.haml26
-rw-r--r--app/views/projects/compare/_form.html.haml2
-rw-r--r--app/views/projects/compare/index.html.haml8
-rw-r--r--app/views/projects/compare/show.html.haml13
-rw-r--r--app/views/projects/diffs/_diffs.html.haml17
-rw-r--r--app/views/projects/diffs/_file.html.haml22
-rw-r--r--app/views/projects/diffs/_stats.html.haml32
-rw-r--r--app/views/projects/edit.html.haml7
-rw-r--r--app/views/projects/empty.html.haml76
-rw-r--r--app/views/projects/forks/new.html.haml21
-rw-r--r--app/views/projects/graphs/_head.html.haml4
-rw-r--r--app/views/projects/graphs/_header_title.html.haml1
-rw-r--r--app/views/projects/graphs/ci.html.haml7
-rw-r--r--app/views/projects/graphs/ci/_build_times.haml21
-rw-r--r--app/views/projects/graphs/ci/_builds.haml41
-rw-r--r--app/views/projects/graphs/ci/_overall.haml22
-rw-r--r--app/views/projects/graphs/commits.html.haml79
-rw-r--r--app/views/projects/graphs/show.html.haml3
-rw-r--r--app/views/projects/imports/show.html.haml8
-rw-r--r--app/views/projects/issues/_discussion.html.haml23
-rw-r--r--app/views/projects/issues/_header_title.html.haml1
-rw-r--r--app/views/projects/issues/_issue.html.haml2
-rw-r--r--app/views/projects/issues/_issues.html.haml11
-rw-r--r--app/views/projects/issues/index.html.haml6
-rw-r--r--app/views/projects/issues/new.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml34
-rw-r--r--app/views/projects/labels/_header_title.html.haml1
-rw-r--r--app/views/projects/labels/edit.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml16
-rw-r--r--app/views/projects/labels/new.html.haml2
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml17
-rw-r--r--app/views/projects/merge_requests/_header_title.html.haml1
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml7
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml11
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml9
-rw-r--r--app/views/projects/merge_requests/_show.html.haml11
-rw-r--r--app/views/projects/merge_requests/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/index.html.haml6
-rw-r--r--app/views/projects/merge_requests/invalid.html.haml2
-rw-r--r--app/views/projects/merge_requests/new.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml16
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml21
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml9
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml57
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml2
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/milestones/_form.html.haml2
-rw-r--r--app/views/projects/milestones/_header_title.html.haml1
-rw-r--r--app/views/projects/milestones/_milestone.html.haml42
-rw-r--r--app/views/projects/milestones/edit.html.haml1
-rw-r--r--app/views/projects/milestones/index.html.haml26
-rw-r--r--app/views/projects/milestones/new.html.haml1
-rw-r--r--app/views/projects/milestones/show.html.haml2
-rw-r--r--app/views/projects/network/show.html.haml1
-rw-r--r--app/views/projects/network/show.json.erb2
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/notes/_edit_form.html.haml2
-rw-r--r--app/views/projects/notes/_form.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml21
-rw-r--r--app/views/projects/project_members/_header_title.html.haml1
-rw-r--r--app/views/projects/project_members/import.html.haml2
-rw-r--r--app/views/projects/project_members/index.html.haml45
-rw-r--r--app/views/projects/runners/_runner.html.haml34
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml23
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml29
-rw-r--r--app/views/projects/runners/edit.html.haml27
-rw-r--r--app/views/projects/runners/index.html.haml25
-rw-r--r--app/views/projects/runners/show.html.haml64
-rw-r--r--app/views/projects/show.html.haml27
-rw-r--r--app/views/projects/snippets/_header_title.html.haml1
-rw-r--r--app/views/projects/snippets/edit.html.haml2
-rw-r--r--app/views/projects/snippets/index.html.haml2
-rw-r--r--app/views/projects/snippets/new.html.haml2
-rw-r--r--app/views/projects/snippets/show.html.haml2
-rw-r--r--app/views/projects/tags/_tag.html.haml12
-rw-r--r--app/views/projects/tags/index.html.haml12
-rw-r--r--app/views/projects/tags/new.html.haml2
-rw-r--r--app/views/projects/tree/_tree.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml1
-rw-r--r--app/views/projects/triggers/_trigger.html.haml14
-rw-r--r--app/views/projects/triggers/index.html.haml67
-rw-r--r--app/views/projects/variables/show.html.haml39
-rw-r--r--app/views/projects/wikis/_form.html.haml2
-rw-r--r--app/views/projects/wikis/_header_title.html.haml1
-rw-r--r--app/views/projects/wikis/_main_links.html.haml11
-rw-r--r--app/views/projects/wikis/_nav.html.haml15
-rw-r--r--app/views/projects/wikis/edit.html.haml2
-rw-r--r--app/views/projects/wikis/empty.html.haml2
-rw-r--r--app/views/projects/wikis/git_access.html.haml21
-rw-r--r--app/views/projects/wikis/history.html.haml11
-rw-r--r--app/views/projects/wikis/pages.html.haml9
-rw-r--r--app/views/projects/wikis/show.html.haml25
-rw-r--r--app/views/shared/_clone_panel.html.haml8
-rw-r--r--app/views/shared/_event_filter.html.haml2
-rw-r--r--app/views/shared/_field.html.haml7
-rw-r--r--app/views/shared/_milestones_filter.html.haml7
-rw-r--r--app/views/shared/groups/_group.html.haml8
-rw-r--r--app/views/shared/issuable/_filter.html.haml9
-rw-r--r--app/views/shared/issuable/_form.html.haml4
-rw-r--r--app/views/shared/projects/_list.html.haml5
-rw-r--r--app/views/shared/projects/_project.html.haml18
-rw-r--r--app/views/snippets/_head.html.haml7
-rw-r--r--app/views/snippets/current_user_index.html.haml35
-rw-r--r--app/views/snippets/index.html.haml16
-rw-r--r--app/views/snippets/show.html.haml4
-rw-r--r--app/views/snippets/user_index.html.haml13
-rw-r--r--app/views/users/show.html.haml17
-rw-r--r--app/workers/ci/hip_chat_notifier_worker.rb19
-rw-r--r--app/workers/ci/slack_notifier_worker.rb10
-rw-r--r--app/workers/ci/web_hook_worker.rb9
-rw-r--r--app/workers/email_receiver_worker.rb2
-rw-r--r--app/workers/fork_registration_worker.rb12
-rw-r--r--app/workers/repository_fork_worker.rb34
-rw-r--r--app/workers/repository_import_worker.rb35
-rwxr-xr-xbin/background_jobs2
-rw-r--r--bin/ci/upgrade.rb3
-rw-r--r--builds/.gitkeep (renamed from tmp/.gitkeep)0
-rw-r--r--config/environments/development.rb5
-rw-r--r--config/gitlab.yml.example81
-rw-r--r--config/initializers/1_settings.rb33
-rw-r--r--config/initializers/3_grit_ext.rb5
-rw-r--r--config/initializers/4_ci_app.rb10
-rw-r--r--config/initializers/connection_fix.rb32
-rw-r--r--config/initializers/cookies_serializer.rb3
-rw-r--r--config/initializers/default_url_options.rb (renamed from config/initializers/8_default_url_options.rb)2
-rw-r--r--config/initializers/devise.rb6
-rw-r--r--config/initializers/omniauth.rb (renamed from config/initializers/7_omniauth.rb)0
-rw-r--r--config/initializers/rack_attack.rb.example14
-rw-r--r--config/initializers/rack_profiler.rb (renamed from config/initializers/6_rack_profiler.rb)2
-rw-r--r--config/initializers/secret_token.rb24
-rw-r--r--config/initializers/session_store.rb9
-rw-r--r--config/initializers/sidekiq.rb (renamed from config/initializers/4_sidekiq.rb)0
-rw-r--r--config/initializers/static_files.rb2
-rw-r--r--config/locales/devise.en.yml103
-rw-r--r--config/mail_room.yml.example2
-rw-r--r--config/routes.rb164
-rw-r--r--config/schedule.rb8
-rw-r--r--config/secrets.yml.example12
-rw-r--r--config/sidekiq.yml.example2
-rw-r--r--db/fixtures/production/001_admin.rb2
-rw-r--r--db/migrate/20150817163600_deduplicate_user_identities.rb2
-rw-r--r--db/migrate/20150826001931_add_ci_tables.rb190
-rw-r--r--db/migrate/20150902001023_add_template_to_label.rb5
-rw-r--r--db/migrate/20150914215247_add_ci_tags.rb23
-rw-r--r--db/migrate/20150915001905_enable_ssl_verification_by_default.rb5
-rw-r--r--db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb8
-rw-r--r--db/migrate/20150916114643_add_help_page_text_to_application_settings.rb5
-rw-r--r--db/migrate/20150916145038_add_index_for_committed_at_and_id.rb5
-rw-r--r--db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb5
-rw-r--r--db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb5
-rw-r--r--db/migrate/20150920010715_add_consumed_timestep_to_users.rb5
-rw-r--r--db/migrate/20150920161119_add_line_code_to_sent_notification.rb5
-rw-r--r--db/migrate/20150924125150_add_project_id_to_ci_commit.rb5
-rw-r--r--db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb6
-rw-r--r--db/migrate/20150930001110_merge_request_error_field.rb5
-rw-r--r--db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb9
-rw-r--r--db/migrate/20151002112914_add_stage_idx_to_builds.rb5
-rw-r--r--db/migrate/20151002121400_add_index_for_builds.rb5
-rw-r--r--db/migrate/20151002122929_add_ref_and_tag_to_builds.rb6
-rw-r--r--db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb6
-rw-r--r--db/migrate/20151005075649_add_user_id_to_build.rb5
-rw-r--r--db/migrate/20151005150751_add_layout_option_for_users.rb5
-rw-r--r--db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb5
-rw-r--r--db/migrate/limits_to_mysql.rb4
-rw-r--r--db/schema.rb224
-rw-r--r--doc/README.md25
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/keys.md46
-rw-r--r--doc/api/projects.md6
-rw-r--r--doc/api/services.md666
-rw-r--r--doc/ci/README.md23
-rw-r--r--doc/ci/api/README.md86
-rw-r--r--doc/ci/api/builds.md41
-rw-r--r--doc/ci/api/commits.md101
-rw-r--r--doc/ci/api/projects.md149
-rw-r--r--doc/ci/api/runners.md77
-rw-r--r--doc/ci/deployment/README.md98
-rw-r--r--doc/ci/docker/README.md4
-rw-r--r--doc/ci/docker/using_docker_build.md111
-rw-r--r--doc/ci/docker/using_docker_images.md203
-rw-r--r--doc/ci/examples/README.md5
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md72
-rw-r--r--doc/ci/examples/test-and-deploy-ruby-application-to-heroku.md67
-rw-r--r--doc/ci/examples/test-clojure-application.md35
-rw-r--r--doc/ci/permissions/README.md24
-rw-r--r--doc/ci/quick_start/README.md119
-rw-r--r--doc/ci/quick_start/build_status.pngbin0 -> 62140 bytes
-rw-r--r--doc/ci/quick_start/commit_status.pngbin0 -> 33492 bytes
-rw-r--r--doc/ci/quick_start/new_commit.pngbin0 -> 47527 bytes
-rw-r--r--doc/ci/quick_start/projects.pngbin0 -> 37014 bytes
-rw-r--r--doc/ci/quick_start/runners.pngbin0 -> 123048 bytes
-rw-r--r--doc/ci/quick_start/runners_activated.pngbin0 -> 60769 bytes
-rw-r--r--doc/ci/runners/README.md145
-rw-r--r--doc/ci/runners/project_specific.pngbin0 -> 31408 bytes
-rw-r--r--doc/ci/runners/shared_runner.pngbin0 -> 18366 bytes
-rw-r--r--doc/ci/runners/shared_to_specific_admin.pngbin0 -> 5897 bytes
-rw-r--r--doc/ci/variables/README.md95
-rw-r--r--doc/ci/yaml/README.md204
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/benchmarking.md69
-rw-r--r--doc/development/rake_tasks.md6
-rw-r--r--doc/gitlab-basics/README.md2
-rw-r--r--doc/gitlab-basics/create-issue.md27
-rw-r--r--doc/gitlab-basics/create-your-ssh-keys.md6
-rw-r--r--doc/hooks/custom_hooks.md2
-rw-r--r--doc/incoming_email/README.md211
-rw-r--r--doc/incoming_email/postfix.md310
-rw-r--r--doc/install/database_mysql.md2
-rw-r--r--doc/install/installation.md71
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/integration/crowd.md58
-rw-r--r--doc/integration/gmail_action_buttons_for_gitlab.md22
-rw-r--r--doc/integration/gmail_actions_button.pngbin0 -> 17321 bytes
-rw-r--r--doc/integration/ldap.md40
-rw-r--r--doc/integration/omniauth.md1
-rw-r--r--doc/markdown/markdown.md7
-rw-r--r--doc/migrate_ci_to_ce/README.md435
-rw-r--r--doc/raketasks/backup_restore.md13
-rw-r--r--doc/raketasks/cleanup.md3
-rw-r--r--doc/release/monthly.md2
-rw-r--r--doc/reply_by_email/README.md180
-rw-r--r--doc/ssh/README.md28
-rw-r--r--doc/update/6.x-or-7.x-to-7.14.md6
-rw-r--r--doc/update/7.13-to-7.14.md2
-rw-r--r--doc/update/7.14-to-8.0.md130
-rw-r--r--doc/update/patch_versions.md9
-rw-r--r--doc/update/upgrader.md2
-rw-r--r--doc/web_hooks/ssl.pngbin0 -> 77165 bytes
-rw-r--r--doc/web_hooks/web_hooks.md28
-rw-r--r--doc/workflow/importing/README.md1
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.pngbin0 -> 53276 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_login.pngbin0 -> 44444 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.pngbin0 -> 35415 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.pngbin0 -> 62552 bytes
-rw-r--r--doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.pngbin0 -> 157856 bytes
-rw-r--r--doc/workflow/importing/import_projects_from_fogbugz.md29
-rw-r--r--docker-compose.yml2
-rw-r--r--docker/.dockerignore1
-rw-r--r--docker/Dockerfile47
-rw-r--r--docker/README.md172
-rwxr-xr-xdocker/assets/wrapper21
-rw-r--r--docker/fig.yml2
-rw-r--r--docker/marathon.json31
-rw-r--r--docker/troubleshooting.md84
-rw-r--r--features/abuse_report.feature7
-rw-r--r--features/admin/labels.feature38
-rw-r--r--features/dashboard/dashboard.feature4
-rw-r--r--features/explore/groups.feature14
-rw-r--r--features/groups.feature11
-rw-r--r--features/login_form.feature5
-rw-r--r--features/project/commits/commits.feature7
-rw-r--r--features/project/graph.feature6
-rw-r--r--features/project/issues/milestones.feature8
-rw-r--r--features/project/merge_requests.feature36
-rw-r--r--features/project/project.feature6
-rw-r--r--features/project/service.feature1
-rw-r--r--features/project/source/browse_files.feature23
-rw-r--r--features/project/wiki.feature12
-rw-r--r--features/steps/abuse_reports.rb4
-rw-r--r--features/steps/admin/labels.rb117
-rw-r--r--features/steps/admin/settings.rb1
-rw-r--r--features/steps/admin/users.rb6
-rw-r--r--features/steps/dashboard/dashboard.rb4
-rw-r--r--features/steps/dashboard/starred_projects.rb2
-rw-r--r--features/steps/groups.rb72
-rw-r--r--features/steps/invites.rb2
-rw-r--r--features/steps/login_form.rb25
-rw-r--r--features/steps/project/commits/commits.rb24
-rw-r--r--features/steps/project/fork.rb5
-rw-r--r--features/steps/project/graph.rb18
-rw-r--r--features/steps/project/issues/issues.rb4
-rw-r--r--features/steps/project/issues/milestones.rb7
-rw-r--r--features/steps/project/merge_requests.rb43
-rw-r--r--features/steps/project/project.rb18
-rw-r--r--features/steps/project/redirects.rb2
-rw-r--r--features/steps/project/services.rb8
-rw-r--r--features/steps/project/source/browse_files.rb83
-rw-r--r--features/steps/project/wiki.rb9
-rw-r--r--features/steps/shared/active_tab.rb4
-rw-r--r--features/steps/shared/diff_note.rb2
-rw-r--r--features/steps/shared/group.rb2
-rw-r--r--features/steps/shared/paths.rb4
-rw-r--r--features/steps/shared/project.rb15
-rw-r--r--features/steps/snippets/user.rb2
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb10
-rw-r--r--lib/api/helpers.rb68
-rw-r--r--lib/api/keys.rb20
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/project_hooks.rb6
-rw-r--r--lib/api/projects.rb36
-rw-r--r--lib/api/services.rb86
-rw-r--r--lib/api/users.rb13
-rw-r--r--lib/backup/builds.rb34
-rw-r--r--lib/backup/database.rb8
-rw-r--r--lib/backup/manager.rb9
-rw-r--r--lib/ci/ansi2html.rb224
-rw-r--r--lib/ci/api/api.rb38
-rw-r--r--lib/ci/api/builds.rb53
-rw-r--r--lib/ci/api/commits.rb66
-rw-r--r--lib/ci/api/entities.rb56
-rw-r--r--lib/ci/api/helpers.rb35
-rw-r--r--lib/ci/api/projects.rb195
-rw-r--r--lib/ci/api/runners.rb69
-rw-r--r--lib/ci/api/triggers.rb49
-rw-r--r--lib/ci/assets/.gitkeep0
-rw-r--r--lib/ci/charts.rb71
-rw-r--r--lib/ci/current_settings.rb22
-rw-r--r--lib/ci/git.rb5
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb199
-rw-r--r--lib/ci/migrate/builds.rb29
-rw-r--r--lib/ci/migrate/database.rb67
-rw-r--r--lib/ci/migrate/manager.rb72
-rw-r--r--lib/ci/migrate/tags.rb42
-rw-r--r--lib/ci/model.rb11
-rw-r--r--lib/ci/scheduler.rb16
-rw-r--r--lib/ci/static_model.rb49
-rw-r--r--lib/ci/version_info.rb52
-rw-r--r--lib/event_filter.rb2
-rw-r--r--lib/gitlab/backend/grack_auth.rb33
-rw-r--r--lib/gitlab/contributions_calendar.rb1
-rw-r--r--lib/gitlab/current_settings.rb14
-rw-r--r--lib/gitlab/diff/file.rb8
-rw-r--r--lib/gitlab/diff/line.rb8
-rw-r--r--lib/gitlab/diff/parser.rb2
-rw-r--r--lib/gitlab/email/receiver.rb5
-rw-r--r--lib/gitlab/fogbugz_import/client.rb56
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb298
-rw-r--r--lib/gitlab/fogbugz_import/project_creator.rb38
-rw-r--r--lib/gitlab/fogbugz_import/repository.rb31
-rw-r--r--lib/gitlab/google_code_import/project_creator.rb2
-rw-r--r--lib/gitlab/import_sources.rb1
-rw-r--r--lib/gitlab/incoming_email.rb (renamed from lib/gitlab/reply_by_email.rb)20
-rw-r--r--lib/gitlab/ldap/auth_hash.rb36
-rw-r--r--lib/gitlab/ldap/config.rb4
-rw-r--r--lib/gitlab/ldap/user.rb6
-rw-r--r--lib/gitlab/markdown.rb2
-rw-r--r--lib/gitlab/markdown/commit_range_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/commit_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/label_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/merge_request_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/relative_link_filter.rb40
-rw-r--r--lib/gitlab/markdown/sanitization_filter.rb12
-rw-r--r--lib/gitlab/markdown/snippet_reference_filter.rb2
-rw-r--r--lib/gitlab/markdown/syntax_highlight_filter.rb8
-rw-r--r--lib/gitlab/markdown/user_reference_filter.rb2
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb22
-rw-r--r--lib/gitlab/url_builder.rb10
-rw-r--r--lib/support/nginx/gitlab9
-rw-r--r--lib/support/nginx/gitlab-ssl9
-rw-r--r--lib/support/nginx/gitlab_ci29
-rw-r--r--lib/tasks/ci/.gitkeep0
-rw-r--r--lib/tasks/ci/cleanup.rake8
-rw-r--r--lib/tasks/ci/migrate.rake87
-rw-r--r--lib/tasks/ci/schedule_builds.rake6
-rw-r--r--lib/tasks/gitlab/backup.rake21
-rw-r--r--lib/tasks/gitlab/check.rake36
-rw-r--r--lib/tasks/gitlab/cleanup.rake49
-rw-r--r--lib/tasks/services.rake98
-rw-r--r--lib/tasks/spec.rake13
-rw-r--r--public/ci/build-canceled.svg1
-rw-r--r--public/ci/build-failed.svg1
-rw-r--r--public/ci/build-pending.svg1
-rw-r--r--public/ci/build-running.svg1
-rw-r--r--public/ci/build-skipped.svg1
-rw-r--r--public/ci/build-success.svg1
-rw-r--r--public/ci/build-unknown.svg1
-rw-r--r--public/ci/favicon.icobin0 -> 5430 bytes
-rwxr-xr-xscripts/ci/prepare_build.sh22
-rw-r--r--spec/benchmarks/models/user_spec.rb40
-rw-r--r--spec/controllers/admin/users_controller_spec.rb15
-rw-r--r--spec/controllers/ci/commits_controller_spec.rb23
-rw-r--r--spec/controllers/import/fogbugz_controller_spec.rb39
-rw-r--r--spec/controllers/namespaces_controller_spec.rb10
-rw-r--r--spec/controllers/profiles/two_factor_auths_controller_spec.rb4
-rw-r--r--spec/controllers/projects/compare_controller_spec.rb26
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb14
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb10
-rw-r--r--spec/controllers/projects/raw_controller_spec.rb37
-rw-r--r--spec/controllers/projects_controller_spec.rb14
-rw-r--r--spec/controllers/root_controller_spec.rb32
-rw-r--r--spec/controllers/uploads_controller_spec.rb8
-rw-r--r--spec/factories/abuse_reports.rb12
-rw-r--r--spec/factories/ci/builds.rb53
-rw-r--r--spec/factories/ci/commits.rb49
-rw-r--r--spec/factories/ci/events.rb24
-rw-r--r--spec/factories/ci/projects.rb44
-rw-r--r--spec/factories/ci/runner_projects.rb19
-rw-r--r--spec/factories/ci/runners.rb38
-rw-r--r--spec/factories/ci/trigger_requests.rb13
-rw-r--r--spec/factories/ci/triggers.rb9
-rw-r--r--spec/factories/ci/web_hook.rb6
-rw-r--r--spec/factories/merge_requests.rb1
-rw-r--r--spec/factories/notes.rb1
-rw-r--r--spec/features/admin/admin_users_spec.rb21
-rw-r--r--spec/features/atom/dashboard_spec.rb4
-rw-r--r--spec/features/builds_spec.rb22
-rw-r--r--spec/features/ci/admin/builds_spec.rb70
-rw-r--r--spec/features/ci/admin/events_spec.rb20
-rw-r--r--spec/features/ci/admin/projects_spec.rb19
-rw-r--r--spec/features/ci/admin/runners_spec.rb64
-rw-r--r--spec/features/ci/builds_spec.rb31
-rw-r--r--spec/features/ci/events_spec.rb22
-rw-r--r--spec/features/ci/lint_spec.rb28
-rw-r--r--spec/features/ci/projects_spec.rb20
-rw-r--r--spec/features/ci_settings_spec.rb22
-rw-r--r--spec/features/ci_web_hooks_spec.rb27
-rw-r--r--spec/features/commits_spec.rb52
-rw-r--r--spec/features/login_spec.rb2
-rw-r--r--spec/features/password_reset_spec.rb62
-rw-r--r--spec/features/profiles/preferences_spec.rb2
-rw-r--r--spec/features/runners_spec.rb87
-rw-r--r--spec/features/security/dashboard_access_spec.rb4
-rw-r--r--spec/features/security/group_access_spec.rb6
-rw-r--r--spec/features/triggers_spec.rb29
-rw-r--r--spec/features/users_spec.rb4
-rw-r--r--spec/features/variables_spec.rb25
-rw-r--r--spec/helpers/ci_status_helper_spec.rb18
-rw-r--r--spec/helpers/gitlab_markdown_helper_spec.rb37
-rw-r--r--spec/helpers/graph_helper_spec.rb16
-rw-r--r--spec/helpers/merge_requests_helper.rb12
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb32
-rw-r--r--spec/helpers/preferences_helper_spec.rb8
-rw-r--r--spec/helpers/projects_helper_spec.rb4
-rw-r--r--spec/helpers/runners_helper_spec.rb18
-rw-r--r--spec/helpers/time_helper_spec.rb37
-rw-r--r--spec/javascripts/syntax_highlight_spec.js.coffee42
-rw-r--r--spec/javascripts/zen_mode_spec.js.coffee5
-rw-r--r--spec/lib/ci/ansi2html_spec.rb134
-rw-r--r--spec/lib/ci/charts_spec.rb16
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb316
-rw-r--r--spec/lib/extracts_path_spec.rb2
-rw-r--r--spec/lib/gitlab/backend/grack_auth_spec.rb10
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb2
-rw-r--r--spec/lib/gitlab/incoming_email_spec.rb61
-rw-r--r--spec/lib/gitlab/ldap/auth_hash_spec.rb65
-rw-r--r--spec/lib/gitlab/ldap/user_spec.rb2
-rw-r--r--spec/lib/gitlab/markdown/relative_link_filter_spec.rb26
-rw-r--r--spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb19
-rw-r--r--spec/lib/gitlab/o_auth/auth_hash_spec.rb6
-rw-r--r--spec/lib/gitlab/o_auth/user_spec.rb6
-rw-r--r--spec/lib/gitlab/reply_by_email_spec.rb86
-rw-r--r--spec/mailers/ci/notify_spec.rb35
-rw-r--r--spec/mailers/notify_spec.rb9
-rw-r--r--spec/models/abuse_report_spec.rb24
-rw-r--r--spec/models/application_setting_spec.rb1
-rw-r--r--spec/models/broadcast_message_spec.rb4
-rw-r--r--spec/models/ci/build_spec.rb387
-rw-r--r--spec/models/ci/commit_spec.rb307
-rw-r--r--spec/models/ci/project_services/hip_chat_message_spec.rb39
-rw-r--r--spec/models/ci/project_services/hip_chat_service_spec.rb73
-rw-r--r--spec/models/ci/project_services/mail_service_spec.rb191
-rw-r--r--spec/models/ci/project_services/slack_message_spec.rb43
-rw-r--r--spec/models/ci/project_services/slack_service_spec.rb57
-rw-r--r--spec/models/ci/project_spec.rb263
-rw-r--r--spec/models/ci/runner_project_spec.rb16
-rw-r--r--spec/models/ci/runner_spec.rb70
-rw-r--r--spec/models/ci/service_spec.rb48
-rw-r--r--spec/models/ci/trigger_spec.rb17
-rw-r--r--spec/models/ci/variable_spec.rb45
-rw-r--r--spec/models/ci/web_hook_spec.rb63
-rw-r--r--spec/models/concerns/issuable_spec.rb16
-rw-r--r--spec/models/hooks/project_hook_spec.rb4
-rw-r--r--spec/models/hooks/service_hook_spec.rb2
-rw-r--r--spec/models/hooks/web_hook_spec.rb2
-rw-r--r--spec/models/issue_spec.rb27
-rw-r--r--spec/models/merge_request_spec.rb1
-rw-r--r--spec/models/milestone_spec.rb6
-rw-r--r--spec/models/note_spec.rb1
-rw-r--r--spec/models/project_services/buildkite_service_spec.rb14
-rw-r--r--spec/models/project_services/drone_ci_service_spec.rb104
-rw-r--r--spec/models/project_services/gitlab_ci_service_spec.rb67
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb4
-rw-r--r--spec/models/project_spec.rb24
-rw-r--r--spec/models/project_team_spec.rb12
-rw-r--r--spec/models/project_wiki_spec.rb2
-rw-r--r--spec/models/user_spec.rb131
-rw-r--r--spec/models/wiki_page_spec.rb2
-rw-r--r--spec/requests/api/keys_spec.rb39
-rw-r--r--spec/requests/api/merge_requests_spec.rb25
-rw-r--r--spec/requests/api/project_hooks_spec.rb27
-rw-r--r--spec/requests/api/projects_spec.rb2
-rw-r--r--spec/requests/api/services_spec.rb81
-rw-r--r--spec/requests/api/users_spec.rb70
-rw-r--r--spec/requests/ci/api/builds_spec.rb121
-rw-r--r--spec/requests/ci/api/commits_spec.rb65
-rw-r--r--spec/requests/ci/api/projects_spec.rb266
-rw-r--r--spec/requests/ci/api/runners_spec.rb83
-rw-r--r--spec/requests/ci/api/triggers_spec.rb81
-rw-r--r--spec/requests/ci/builds_spec.rb17
-rw-r--r--spec/requests/ci/commits_spec.rb16
-rw-r--r--spec/routing/routing_spec.rb6
-rw-r--r--spec/services/ci/create_commit_service_spec.rb154
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb38
-rw-r--r--spec/services/ci/event_service_spec.rb34
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb49
-rw-r--r--spec/services/ci/register_build_service_spec.rb90
-rw-r--r--spec/services/ci/web_hook_service_spec.rb37
-rw-r--r--spec/services/event_create_service_spec.rb10
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb14
-rw-r--r--spec/services/notification_service_spec.rb6
-rw-r--r--spec/services/projects/create_service_spec.rb19
-rw-r--r--spec/services/projects/download_service_spec.rb65
-rw-r--r--spec/services/projects/fork_service_spec.rb16
-rw-r--r--spec/spec_helper.rb9
-rw-r--r--spec/support/api_helpers.rb11
-rw-r--r--spec/support/filter_spec_helper.rb2
-rw-r--r--spec/support/gitlab_stubs/gitlab_ci.yml63
-rw-r--r--spec/support/gitlab_stubs/project_8.json45
-rw-r--r--spec/support/gitlab_stubs/project_8_hooks.json1
-rw-r--r--spec/support/gitlab_stubs/projects.json1
-rw-r--r--spec/support/gitlab_stubs/session.json20
-rw-r--r--spec/support/gitlab_stubs/user.json20
-rw-r--r--spec/support/login_helpers.rb4
-rw-r--r--spec/support/matchers/benchmark_matchers.rb61
-rw-r--r--spec/support/matchers/markdown_matchers.rb3
-rw-r--r--spec/support/relative_url.rb8
-rw-r--r--spec/support/services_shared_context.rb21
-rw-r--r--spec/support/setup_builds_storage.rb19
-rw-r--r--spec/support/stub_configuration.rb4
-rw-r--r--spec/support/stub_gitlab_calls.rb85
-rw-r--r--spec/support/stub_gitlab_data.rb5
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb16
-rw-r--r--spec/views/help/index.html.haml_spec.rb41
-rw-r--r--spec/workers/email_receiver_worker_spec.rb4
-rw-r--r--spec/workers/fork_registration_worker_spec.rb10
-rw-r--r--spec/workers/repository_fork_worker_spec.rb29
947 files changed, 24469 insertions, 3889 deletions
diff --git a/.gitignore b/.gitignore
index 8a68bb3e4f0..2a97eacad48 100644
--- a/.gitignore
+++ b/.gitignore
@@ -20,12 +20,13 @@ backups/*
config/aws.yml
config/database.yml
config/gitlab.yml
-config/initializers/omniauth.rb
+config/gitlab_ci.yml
config/initializers/rack_attack.rb
config/initializers/smtp_settings.rb
config/resque.yml
config/unicorn.rb
config/mail_room.yml
+config/secrets.yml
coverage/*
db/*.sqlite3
db/*.sqlite3-journal
@@ -41,3 +42,4 @@ rails_best_practices_output.html
/tags
tmp/
vendor/bundle/*
+builds/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ddf4e31204a..cf6d28b01af 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -24,6 +24,14 @@ spec:api:
- ruby
- mysql
+spec:benchmark:
+ script:
+ - RAILS_ENV=test bundle exec rake spec:benchmark
+ tags:
+ - ruby
+ - mysql
+ allow_failure: true
+
spec:other:
script:
- RAILS_ENV=test SIMPLECOV=true bundle exec rake spec:other
diff --git a/.rubocop.yml b/.rubocop.yml
index ea4d365761e..11e4502849a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -932,7 +932,7 @@ Lint/UselessAccessModifier:
Lint/UselessAssignment:
Description: 'Checks for useless assignment to a local variable.'
StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#underscore-unused-vars'
- Enabled: false
+ Enabled: true
Lint/UselessComparison:
Description: 'Checks for comparison of something with itself.'
@@ -998,7 +998,9 @@ AllCops:
- 'tmp/**/*'
- 'bin/**/*'
- 'lib/backup/**/*'
+ - 'lib/ci/backup/**/*'
- 'lib/tasks/**/*'
+ - 'lib/ci/migrate/**/*'
- 'lib/email_validator.rb'
- 'lib/gitlab/upgrader.rb'
- 'lib/gitlab/seeder.rb'
diff --git a/CHANGELOG b/CHANGELOG
index 8c390c16205..388fa2f8966 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,14 +1,91 @@
Please view this file on the master branch, on stable branches it's out of date.
-v 8.0.0 (unreleased)
+v 8.1.0 (unreleased)
+ - Fix bug where transferring a project would result in stale commit links (Stan Hu)
+ - Include full path of source and target branch names in New Merge Request page (Stan Hu)
+ - Add user preference to view activities as default dashboard (Stan Hu)
+ - Add option to admin area to sign in as a specific user (Pavel Forkert)
+ - Show CI status on all pages where commits list is rendered
+ - Automatically enable CI when push .gitlab-ci.yml file to repository
+ - Move CI charts to project graphs area
+ - Fix cases where Markdown did not render links in activity feed (Stan Hu)
+ - Add first and last to pagination (Zeger-Jan van de Weg)
+ - Show CI status on commit page
+ - Show CI status on Your projects page and Starred projects page
+ - Remove "Continuous Integration" page from dashboard
+ - Add notes and SSL verification entries to hook APIs (Ben Boeckel)
+ - Fix grammar in admin area "labels" .nothing-here-block when no labels exist.
+ - Move CI runners page to project settings area
+ - Move CI variables page to project settings area
+ - Move CI triggers page to project settings area
+ - Move CI project settings page to CE project settings area
+ - Fix bug when removed file was not appearing in merge request diff
+ - Note the original location of a moved project when notifying users of the move
+ - Improve error message when merging fails
+ - Add support of multibyte characters in LDAP UID (Roman Petrov)
+ - Show additions/deletions stats on merge request diff
+ - Remove footer text in emails (Zeger-Jan van de Weg)
+ - Ensure code blocks are properly highlighted after a note is updated
+ - Fix wrong access level badge on MR comments
+ - Hide password in the service settings form
+ - Move CI web hooks page to project settings area
+ - Fix User Identities API. It now allows you to properly create or update user's identities.
+ - Add user preference to change layout width (Peter Göbel)
+ - Use commit status in merge request widget as preffered source of CI status
+ - Integrate CI commit and build pages into project pages
+
+v 8.0.4
+ - Fix Message-ID header to be RFC 2111-compliant to prevent e-mails being dropped (Stan Hu)
+ - Fix referrals for :back and relative URL installs
+ - Fix anchors to comments in diffs
+ - Remove CI token from build traces
+ - Fix "Assign All" button on Runner admin page
+ - Fix search in Files
+
+v 8.0.3
+ - Fix URL shown in Slack notifications
+ - Fix bug where projects would appear to be stuck in the forked import state (Stan Hu)
+ - Fix Error 500 in creating merge requests with > 1000 diffs (Stan Hu)
+
+v 8.0.2
+ - Fix default avatar not rendering in network graph (Stan Hu)
+ - Skip check_initd_configured_correctly on omnibus installs
+ - Prevent double-prefixing of help page paths
+ - Clarify confirmation text on user deletion
+ - Make commit graphs responsive to window width changes (Stan Hu)
+ - Fix top margin for sign-in button on public pages
+ - Fix LDAP attribute mapping
+ - Remove git refs used internally by GitLab from network graph (Stan Hu)
+ - Use standard Markdown font in Markdown preview instead of fixed-width font (Stan Hu)
+ - Fix Reply by email for non-UTF-8 messages.
+ - Add option to use StartTLS with Reply by email IMAP server.
+ - Allow AWS S3 Server-Side Encryption with Amazon S3-Managed Keys for backups (Paul Beattie)
+
+v 8.0.1
+ - Remove git refs used internally by GitLab from network graph (Stan Hu)
+ - Improve CI migration procedure and documentation
+
+v 8.0.0
+ - Fix Markdown links not showing up in dashboard activity feed (Stan Hu)
+ - Remove milestones from merge requests when milestones are deleted (Stan Hu)
+ - Fix HTML link that was improperly escaped in new user e-mail (Stan Hu)
+ - Fix broken sort in merge request API (Stan Hu)
+ - Bump rouge to 1.10.1 to remove warning noise and fix other syntax highlighting bugs (Stan Hu)
+ - Gracefully handle errors in syntax highlighting by leaving the block unformatted (Stan Hu)
+ - Add "replace" and "upload" functionalities to allow user replace existing file and upload new file into current repository
+ - Fix URL construction for merge requests, issues, notes, and commits for relative URL config (Stan Hu)
+ - Fix emoji URLs in Markdown when relative_url_root is used (Stan Hu)
+ - Omit filename in Content-Disposition header in raw file download to avoid RFC 6266 encoding issues (Stan HU)
+ - Fix broken Wiki Page History (Stan Hu)
+ - Import forked repositories asynchronously to prevent large repositories from timing out (Stan Hu)
- Prevent anchors from being hidden by header (Stan Hu)
- Fix bug where only the first 15 Bitbucket issues would be imported (Stan Hu)
- Sort issues by creation date in Bitbucket importer (Stan Hu)
- - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu)
- Prevent too many redirects upon login when home page URL is set to external_url (Stan Hu)
- Improve dropdown positioning on the project home page (Hannes Rosenögger)
- Upgrade browser gem to 1.0.0 to avoid warning in IE11 compatibilty mode (Stan Hu)
- Remove user OAuth tokens from the database and request new tokens each session (Stan Hu)
+ - Restrict users API endpoints to use integer IDs (Stan Hu)
- Only show recent push event if the branch still exists or a recent merge request has not been created (Stan Hu)
- Remove satellites
- Better performance for web editor (switched from satellites to rugged)
@@ -20,20 +97,50 @@ v 8.0.0 (unreleased)
- Create cross-reference for closing references on commits pushed to non-default branches (Maël Valais)
- Ability to search milestones
- Gracefully handle SMTP user input errors (e.g. incorrect email addresses) to prevent Sidekiq retries (Stan Hu)
- - Move dashboard activity to separate page
+ - Move dashboard activity to separate page (for your projects and starred projects)
- Improve performance of git blame
- Limit content width to 1200px for most of pages to improve readability on big screens
- Fix 500 error when submit project snippet without body
- Improve search page usability
- Bring more UI consistency in way how projects, snippets and groups lists are rendered
- - Make all profiles public
+ - Make all profiles and group public
- Fixed login failure when extern_uid changes (Joel Koglin)
- Don't notify users without access to the project when they are (accidentally) mentioned in a note.
- Retrieving oauth token with LDAP credentials
+ - Load Application settings from running database unless env var USE_DB=false
+ - Added Drone CI integration (Kirill Zaitsev)
+ - Allow developers to retry builds
+ - Hide advanced project options for non-admin users
+ - Fail builds if no .gitlab-ci.yml is found
+ - Refactored service API and added automatically service docs generator (Kirill Zaitsev)
+ - Added web_url key project hook_attrs (Kirill Zaitsev)
+ - Add ability to get user information by ID of an SSH key via the API
+ - Fix bug which IE cannot show image at markdown when the image is raw file of gitlab
+ - Add support for Crowd
+ - Global Labels that are available to all projects
+ - Fix highlighting of deleted lines in diffs.
+ - Project notification level can be set on the project page itself
+ - Added service API endpoint to retrieve service parameters (Petheő Bence)
+ - Add FogBugz project import (Jared Szechy)
+ - Sort users autocomplete lists by user (Allister Antosik)
+ - Webhook for issue now contains repository field (Jungkook Park)
+ - Add ability to add custom text to the help page (Jeroen van Baarsen)
+ - Add pg_schema to backup config
+ - Fix references to target project issues in Merge Requests markdown preview and textareas (Francesco Levorato)
+ - Redirect from incorrectly cased group or project path to correct one (Francesco Levorato)
+ - Removed API calls from CE to CI
+
+v 7.14.3
+ - No changes
+
+v 7.14.2
+ - Upgrade gitlab_git to 7.2.15 to fix `git blame` errors with ISO-encoded files (Stan Hu)
+ - Allow configuration of LDAP attributes GitLab will use for the new user account.
v 7.14.1
- Improve abuse reports management from admin area
- Fix "Reload with full diff" URL button in compare branch view (Stan Hu)
+ - Disabled DNS lookups for SSH in docker image (Rowan Wookey)
- Only include base URL in OmniAuth full_host parameter (Stan Hu)
- Fix Error 500 in API when accessing a group that has an avatar (Stan Hu)
- Ability to enable SSL verification for Webhooks
@@ -114,7 +221,7 @@ v 7.13.4
v 7.13.3
- Fix bug causing Bitbucket importer to crash when OAuth application had been removed.
- Allow users to send abuse reports
- - Remove satellites
+ - Remove satellites
- Link username to profile on Group Members page (Tom Webster)
v 7.13.2
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 2714f5313ae..57cf282ebbc 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-2.6.4
+2.6.5
diff --git a/Gemfile b/Gemfile
index 2a55bb31cb9..4938cbf8b80 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,40 +18,41 @@ 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.5.2'
+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 'omniauth_crowd'
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', '~> 2.0.0'
+gem 'rqrcode-rails3', '~> 0.1.7'
+gem 'attr_encrypted', '~> 1.3.4'
# Browser detection
gem "browser", '~> 1.0.0'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem "gitlab_git", '~> 7.2.15'
+gem "gitlab_git", '~> 7.2.17'
# 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'
@@ -58,47 +67,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"
+gem "kaminari", "~> 0.16.3"
# 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
@@ -106,37 +115,40 @@ 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'
+# Run events after state machine commits
+gem 'after_commit_queue'
# Issue tags
gem 'acts-as-taggable-on', '~> 3.4'
# Background jobs
-gem 'slim'
-gem 'sinatra', require: nil
-gem 'sidekiq', '~> 3.3'
-gem 'sidetiq', '0.6.3'
+gem 'slim', '~> 2.0.2'
+gem 'sinatra', '~> 1.4.4', require: nil
+gem 'sidekiq', '3.3.0'
+gem 'sidetiq', '~> 0.6.3'
# HTTP requests
-gem "httparty"
+gem "httparty", '~> 0.13.3'
# Colored output to console
-gem "colored"
+gem "colored", '~> 1.2'
+gem "colorize", '~> 0.5.8'
# 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'
@@ -156,6 +168,9 @@ gem "slack-notifier", "~> 1.0.0"
# Asana integration
gem 'asana', '~> 0.0.6'
+# FogBugz integration
+gem 'ruby-fogbugz', '~> 0.2.1'
+
# d3
gem 'd3_rails', '~> 3.5.5'
@@ -172,69 +187,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 '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 "foreman"
+ gem 'brakeman', '3.0.1', 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'
@@ -244,30 +260,59 @@ 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
+
+ gem 'benchmark-ips', 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'
+ gem 'sham_rack'
end
group :production do
gem "gitlab_meta", '7.0'
end
-gem "newrelic_rpm"
+gem "newrelic_rpm", '~> 3.9.4.245'
+gem 'newrelic-grape'
+
+gem 'octokit', '~> 3.7.0'
+
+gem "mail_room", "~> 0.5.2"
+
+gem 'email_reply_parser', '~> 0.5.8'
-gem 'octokit', '3.7.0'
+## CI
+gem 'activerecord-deprecated_finders', '~> 1.0.3'
+gem 'activerecord-session_store', '~> 0.1.0'
+gem "nested_form", '~> 0.3.2'
-gem "mail_room", "~> 0.4.1"
+# Scheduled
+gem 'whenever', '~> 0.8.4', require: false
-gem 'email_reply_parser'
+# OAuth
+gem 'oauth2', '~> 1.0.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 2450b95d973..1dd56cd9c8c 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,33 +42,37 @@ 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)
+ after_commit_queue (1.1.0)
+ rails (>= 3.0)
+ 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)
+ benchmark-ips (2.3.0)
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)
@@ -78,9 +87,7 @@ GEM
terminal-table (~> 1.4)
browser (1.0.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 +95,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 +105,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)
@@ -111,8 +120,7 @@ GEM
coffee-script-source (1.9.1.1)
colored (1.2)
colorize (0.5.8)
- columnize (0.9.0)
- connection_pool (2.1.0)
+ connection_pool (2.2.0)
coveralls (0.8.2)
json (~> 1.8)
rest-client (>= 1.6.8, < 2)
@@ -122,38 +130,37 @@ 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)
- devise (3.2.4)
+ descendants_tracker (0.0.4)
+ thread_safe (~> 0.3, >= 0.3.1)
+ devise (3.5.2)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 3.2.6, < 5)
+ responders
thread_safe (~> 0.1)
warden (~> 1.2.3)
devise-async (0.9.0)
devise (~> 3.2)
- devise-two-factor (1.0.1)
- activemodel
+ devise-two-factor (2.0.0)
activesupport
attr_encrypted (~> 1.3.2)
- devise (~> 3.2.4)
- rails
- rotp (~> 1.6.1)
+ devise (~> 3.5.0)
+ 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 +170,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 +209,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 +223,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 +234,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,28 +249,26 @@ 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-grit (2.7.2)
+ gitlab-grit (2.7.3)
charlock_holmes (~> 0.6)
diff-lcs (~> 1.1)
mime-types (~> 1.15)
@@ -272,9 +277,9 @@ GEM
charlock_holmes (~> 0.6.6)
escape_utils (~> 0.2.4)
mime-types (~> 1.19)
- gitlab_emoji (0.1.0)
+ gitlab_emoji (0.1.1)
gemojione (~> 2.0)
- gitlab_git (7.2.15)
+ gitlab_git (7.2.17)
activesupport (~> 4.0)
charlock_holmes (~> 0.6)
gitlab-linguist (~> 3.0)
@@ -285,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.10.1)
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)
@@ -307,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)
@@ -320,24 +338,23 @@ 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)
+ hitimes (1.2.3)
html-pipeline (1.11.0)
activesupport (>= 2)
nokogiri (~> 1.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)
@@ -346,58 +363,66 @@ 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)
- kaminari (0.15.1)
+ jwt (1.5.1)
+ kaminari (0.16.3)
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)
mime-types (>= 1.16, < 3)
- mail_room (0.4.1)
+ mail_room (0.5.2)
method_source (0.8.2)
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)
net-ssh (2.9.2)
netrc (0.10.3)
+ newrelic-grape (2.0.0)
+ grape
+ newrelic_rpm
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)
@@ -406,34 +431,38 @@ 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)
omniauth-oauth (~> 1.0)
+ omniauth_crowd (2.2.3)
+ activesupport
+ nokogiri (>= 1.4.4)
+ omniauth (~> 1.0)
opennebula (4.12.1)
json
nokogiri
@@ -441,7 +470,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)
@@ -449,60 +480,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.12)
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)
@@ -517,10 +547,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)
@@ -531,32 +561,35 @@ 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)
+ responders (1.1.2)
+ railties (>= 3.2, < 4.2)
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)
+ rotp (2.1.1)
+ rouge (1.10.1)
+ 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)
@@ -571,16 +604,18 @@ GEM
powerpack (~> 0.0.6)
rainbow (>= 1.99.1, < 3.0)
ruby-progressbar (~> 1.4)
- ruby-progressbar (1.7.1)
+ ruby-fogbugz (0.2.1)
+ crack (~> 0.4)
+ 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)
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)
@@ -604,7 +639,10 @@ GEM
select2-rails (3.5.9.3)
thor (~> 0.14)
settingslogic (2.0.9)
- sexp_processor (4.4.5)
+ sexp_processor (4.6.0)
+ sham_rack (1.3.6)
+ rack
+ shellany (0.0.1)
shoulda-matchers (2.8.0)
activesupport (>= 3.0.0)
sidekiq (3.3.0)
@@ -623,19 +661,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)
@@ -666,31 +705,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)
+ timers (4.0.4)
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,35 +740,39 @@ GEM
simple_oauth (~> 0.1.4)
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)
+ whenever (0.8.4)
+ activesupport (>= 2.3.4)
+ chronic (>= 0.6.3)
wikicloth (0.8.1)
builder
expression_parser
@@ -740,141 +784,161 @@ 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)
+ after_commit_queue
+ 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)
+ benchmark-ips
+ better_errors (~> 1.0.1)
+ binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.0)
- brakeman
+ brakeman (= 3.0.1)
browser (~> 1.0.0)
byebug
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)
+ colorize (~> 0.5.8)
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.5.2)
+ devise-async (~> 0.9.0)
+ devise-two-factor (~> 2.0.0)
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-linguist (~> 3.0.1)
gitlab_emoji (~> 0.1)
- gitlab_git (~> 7.2.15)
+ gitlab_git (~> 7.2.17)
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
- kaminari (~> 0.15.1)
- letter_opener
- mail_room (~> 0.4.1)
- minitest (~> 5.3.0)
- mousetrap-rails
- mysql2
- newrelic_rpm
- nprogress-rails
- octokit (= 3.7.0)
+ jquery-rails (~> 3.1.3)
+ jquery-scrollto-rails (~> 1.4.3)
+ jquery-turbolinks (~> 2.0.1)
+ jquery-ui-rails (~> 4.2.1)
+ kaminari (~> 0.16.3)
+ letter_opener (~> 1.1.2)
+ mail_room (~> 0.5.2)
+ minitest (~> 5.7.0)
+ mousetrap-rails (~> 1.4.6)
+ mysql2 (~> 0.3.16)
+ nested_form (~> 0.3.2)
+ newrelic-grape
+ 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)
+ omniauth_crowd
+ 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)
+ ruby-fogbugz (~> 0.2.1)
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)
+ sham_rack
shoulda-matchers (~> 2.8.0)
- sidekiq (~> 3.3)
- sidetiq (= 0.6.3)
- simplecov
- sinatra
- six
+ sidekiq (= 3.3.0)
+ 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)
+ 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/README.md b/README.md
index 52e12bb66ad..91855b42d29 100644
--- a/README.md
+++ b/README.md
@@ -1,9 +1,9 @@
# GitLab
-[![build status](https://ci.gitlab.com/projects/1/status.png?ref=master)](https://ci.gitlab.com/projects/1?ref=master)
+[![build status](https://ci.gitlab.com/projects/1/status.svg?ref=master)](https://ci.gitlab.com/projects/1?ref=master)
[![Build Status](https://semaphoreci.com/api/v1/projects/2f1a5809-418b-4cc2-a1f4-819607579fe7/400484/shields_badge.svg)](https://semaphoreci.com/gitlabhq/gitlabhq)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
-[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.png?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master)
+[![Coverage Status](https://coveralls.io/repos/gitlabhq/gitlabhq/badge.svg?branch=master)](https://coveralls.io/r/gitlabhq/gitlabhq?branch=master)
## Canonical source
diff --git a/VERSION b/VERSION
index 939cbc3ea74..a2264f05f50 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.0.0.pre
+8.1.0.pre
diff --git a/app/assets/fonts/OFL.txt b/app/assets/fonts/OFL.txt
new file mode 100755
index 00000000000..a9b845ed1d4
--- /dev/null
+++ b/app/assets/fonts/OFL.txt
@@ -0,0 +1,92 @@
+Copyright 2010, 2012 Adobe Systems Incorporated (http://www.adobe.com/), with Reserved Font Name 'Source'. All Rights Reserved. Source is a trademark of Adobe Systems Incorporated in the United States and/or other countries.
+This Font Software is licensed under the SIL Open Font License, Version 1.1.
+This license is copied below, and is also available with a FAQ at:
+http://scripts.sil.org/OFL
+
+
+-----------------------------------------------------------
+SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
+-----------------------------------------------------------
+
+PREAMBLE
+The goals of the Open Font License (OFL) are to stimulate worldwide
+development of collaborative font projects, to support the font creation
+efforts of academic and linguistic communities, and to provide a free and
+open framework in which fonts may be shared and improved in partnership
+with others.
+
+The OFL allows the licensed fonts to be used, studied, modified and
+redistributed freely as long as they are not sold by themselves. The
+fonts, including any derivative works, can be bundled, embedded,
+redistributed and/or sold with any software provided that any reserved
+names are not used by derivative works. The fonts and derivatives,
+however, cannot be released under any other type of license. The
+requirement for fonts to remain under this license does not apply
+to any document created using the fonts or their derivatives.
+
+DEFINITIONS
+"Font Software" refers to the set of files released by the Copyright
+Holder(s) under this license and clearly marked as such. This may
+include source files, build scripts and documentation.
+
+"Reserved Font Name" refers to any names specified as such after the
+copyright statement(s).
+
+"Original Version" refers to the collection of Font Software components as
+distributed by the Copyright Holder(s).
+
+"Modified Version" refers to any derivative made by adding to, deleting,
+or substituting -- in part or in whole -- any of the components of the
+Original Version, by changing formats or by porting the Font Software to a
+new environment.
+
+"Author" refers to any designer, engineer, programmer, technical
+writer or other person who contributed to the Font Software.
+
+PERMISSION & CONDITIONS
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of the Font Software, to use, study, copy, merge, embed, modify,
+redistribute, and sell modified and unmodified copies of the Font
+Software, subject to the following conditions:
+
+1) Neither the Font Software nor any of its individual components,
+in Original or Modified Versions, may be sold by itself.
+
+2) Original or Modified Versions of the Font Software may be bundled,
+redistributed and/or sold with any software, provided that each copy
+contains the above copyright notice and this license. These can be
+included either as stand-alone text files, human-readable headers or
+in the appropriate machine-readable metadata fields within text or
+binary files as long as those fields can be easily viewed by the user.
+
+3) No Modified Version of the Font Software may use the Reserved Font
+Name(s) unless explicit written permission is granted by the corresponding
+Copyright Holder. This restriction only applies to the primary font name as
+presented to the users.
+
+4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
+Software shall not be used to promote, endorse or advertise any
+Modified Version, except to acknowledge the contribution(s) of the
+Copyright Holder(s) and the Author(s) or with their explicit written
+permission.
+
+5) The Font Software, modified or unmodified, in part or in whole,
+must be distributed entirely under this license, and must not be
+distributed under any other license. The requirement for fonts to
+remain under this license does not apply to any document created
+using the Font Software.
+
+TERMINATION
+This license becomes null and void if any of the above conditions are
+not met.
+
+DISCLAIMER
+THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
+OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
+COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
+DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
+OTHER DEALINGS IN THE FONT SOFTWARE.
diff --git a/app/assets/fonts/SourceSansPro-Black.ttf b/app/assets/fonts/SourceSansPro-Black.ttf
new file mode 100755
index 00000000000..cb89a2d171e
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Black.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BlackItalic.ttf b/app/assets/fonts/SourceSansPro-BlackItalic.ttf
new file mode 100755
index 00000000000..c719243c0d6
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BlackItalic.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Bold.ttf b/app/assets/fonts/SourceSansPro-Bold.ttf
new file mode 100755
index 00000000000..5d65c93242f
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Bold.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-BoldItalic.ttf b/app/assets/fonts/SourceSansPro-BoldItalic.ttf
new file mode 100755
index 00000000000..d20dd0c5eca
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-BoldItalic.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLight.ttf b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
new file mode 100755
index 00000000000..bb4176c6fff
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLight.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf b/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
new file mode 100755
index 00000000000..2c34f3b8dc4
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-ExtraLightItalic.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Italic.ttf b/app/assets/fonts/SourceSansPro-Italic.ttf
new file mode 100755
index 00000000000..e5a1a86e631
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Italic.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Light.ttf b/app/assets/fonts/SourceSansPro-Light.ttf
new file mode 100755
index 00000000000..83a0a336661
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Light.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-LightItalic.ttf b/app/assets/fonts/SourceSansPro-LightItalic.ttf
new file mode 100755
index 00000000000..88a6778d24f
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-LightItalic.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Regular.ttf b/app/assets/fonts/SourceSansPro-Regular.ttf
new file mode 100755
index 00000000000..44486cdc670
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Regular.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-Semibold.ttf b/app/assets/fonts/SourceSansPro-Semibold.ttf
new file mode 100755
index 00000000000..86b00c067e0
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-Semibold.ttf
Binary files differ
diff --git a/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf b/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf
new file mode 100755
index 00000000000..2c5ad3008c3
--- /dev/null
+++ b/app/assets/fonts/SourceSansPro-SemiboldItalic.ttf
Binary files differ
diff --git a/app/assets/images/ci/arch.jpg b/app/assets/images/ci/arch.jpg
new file mode 100644
index 00000000000..0e05674e840
--- /dev/null
+++ b/app/assets/images/ci/arch.jpg
Binary files differ
diff --git a/app/assets/images/ci/favicon.ico b/app/assets/images/ci/favicon.ico
new file mode 100644
index 00000000000..9663d4d00b9
--- /dev/null
+++ b/app/assets/images/ci/favicon.ico
Binary files differ
diff --git a/app/assets/images/ci/loader.gif b/app/assets/images/ci/loader.gif
new file mode 100644
index 00000000000..2fcb8f2da0d
--- /dev/null
+++ b/app/assets/images/ci/loader.gif
Binary files differ
diff --git a/app/assets/images/ci/no_avatar.png b/app/assets/images/ci/no_avatar.png
new file mode 100644
index 00000000000..752d26adba7
--- /dev/null
+++ b/app/assets/images/ci/no_avatar.png
Binary files differ
diff --git a/app/assets/images/ci/rails.png b/app/assets/images/ci/rails.png
new file mode 100644
index 00000000000..d5edc04e65f
--- /dev/null
+++ b/app/assets/images/ci/rails.png
Binary files differ
diff --git a/app/assets/images/ci/service_sample.png b/app/assets/images/ci/service_sample.png
new file mode 100644
index 00000000000..65d29e3fd89
--- /dev/null
+++ b/app/assets/images/ci/service_sample.png
Binary files differ
diff --git a/app/assets/javascripts/activities.js.coffee b/app/assets/javascripts/activities.js.coffee
index 777c62dc1b7..63803747413 100644
--- a/app/assets/javascripts/activities.js.coffee
+++ b/app/assets/javascripts/activities.js.coffee
@@ -1,7 +1,7 @@
class @Activities
constructor: ->
Pager.init 20, true
- $(".event_filter_link").bind "click", (event) =>
+ $(".event-filter .btn").bind "click", (event) =>
event.preventDefault()
@toggleFilter($(event.currentTarget))
@reloadActivities()
@@ -12,7 +12,7 @@ class @Activities
toggleFilter: (sender) ->
- sender.parent().toggleClass "active"
+ sender.toggleClass "active"
event_filters = $.cookie("event_filter")
filter = sender.attr("id").split("_")[0]
if event_filters
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js.coffee b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
new file mode 100644
index 00000000000..3ab3ba66754
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js.coffee
@@ -0,0 +1,67 @@
+class @BlobFileDropzone
+ constructor: (form, method) ->
+ form_dropzone = form.find('.dropzone')
+ Dropzone.autoDiscover = false
+ dropzone = form_dropzone.dropzone(
+ autoDiscover: false
+ autoProcessQueue: false
+ url: form.attr('action')
+ # Rails uses a hidden input field for PUT
+ # http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
+ method: method
+ clickable: true
+ uploadMultiple: false
+ paramName: "file"
+ maxFilesize: gon.max_file_size or 10
+ parallelUploads: 1
+ maxFiles: 1
+ addRemoveLinks: true
+ previewsContainer: '.dropzone-previews'
+ headers:
+ "X-CSRF-Token": $("meta[name=\"csrf-token\"]").attr("content")
+
+ init: ->
+ this.on 'addedfile', (file) ->
+ $('.dropzone-alerts').html('').hide()
+ commit_message = form.find('#commit_message')[0]
+
+ if /^Upload/.test(commit_message.placeholder)
+ commit_message.placeholder = 'Upload ' + file.name
+
+ return
+
+ this.on 'removedfile', (file) ->
+ commit_message = form.find('#commit_message')[0]
+
+ if /^Upload/.test(commit_message.placeholder)
+ commit_message.placeholder = 'Upload new file'
+
+ return
+
+ this.on 'success', (header, response) ->
+ window.location.href = response.filePath
+ return
+
+ this.on 'maxfilesexceeded', (file) ->
+ @removeFile file
+ return
+
+ this.on 'sending', (file, xhr, formData) ->
+ formData.append('commit_message', form.find('#commit_message').val())
+ return
+
+ # Override behavior of adding error underneath preview
+ error: (file, errorMessage) ->
+ stripped = $("<div/>").html(errorMessage).text();
+ $('.dropzone-alerts').html('Error uploading file: \"' + stripped + '\"').show()
+ @removeFile file
+ return
+ )
+
+ submitButton = form.find('#submit-all')[0]
+ submitButton.addEventListener 'click', (e) ->
+ e.preventDefault()
+ e.stopPropagation()
+ alert "Please select a file" if dropzone[0].dropzone.getQueuedFiles().length == 0
+ dropzone[0].dropzone.processQueue()
+ return false
diff --git a/app/assets/javascripts/ci/Chart.min.js b/app/assets/javascripts/ci/Chart.min.js
new file mode 100644
index 00000000000..ab635881087
--- /dev/null
+++ b/app/assets/javascripts/ci/Chart.min.js
@@ -0,0 +1,39 @@
+var Chart=function(s){function v(a,c,b){a=A((a-c.graphMin)/(c.steps*c.stepValue),1,0);return b*c.steps*a}function x(a,c,b,e){function h(){g+=f;var k=a.animation?A(d(g),null,0):1;e.clearRect(0,0,q,u);a.scaleOverlay?(b(k),c()):(c(),b(k));if(1>=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);d<b||d>c;)a=d<b?a/2:2*a,d=Math.round(e/a);c=[];z(f,c,d,h,a);return{steps:d,stepValue:a,graphMin:h,labels:c}}function z(a,c,b,e,h){if(a)for(var f=1;f<b+1;f++)c.push(E(a,{value:(e+h*f).toFixed(0!=h%1?h.toString().split(".")[1].length:0)}))}function A(a,c,b){return!isNaN(parseFloat(c))&&isFinite(c)&&a>c?c:!isNaN(parseFloat(b))&&
+isFinite(b)&&a<b?b:a}function y(a,c){var b={},e;for(e in a)b[e]=a[e];for(e in c)b[e]=c[e];return b}function E(a,c){var b=!/\W/.test(a)?F[a]=F[a]||E(document.getElementById(a).innerHTML):new Function("obj","var p=[],print=function(){p.push.apply(p,arguments);};with(obj){p.push('"+a.replace(/[\r\t\n]/g," ").split("<%").join("\t").replace(/((^|%>)[^\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);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*
+Math.PI)*Math.asin(1/e);return-(e*Math.pow(2,10*(a-=1))*Math.sin((1*a-c)*2*Math.PI/b))},easeOutElastic: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);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return e*Math.pow(2,-10*a)*Math.sin((1*a-c)*2*Math.PI/b)+1},easeInOutElastic:function(a){var c=1.70158,b=0,e=1;if(0==a)return 0;if(2==(a/=0.5))return 1;b||(b=1*0.3*1.5);e<Math.abs(1)?(e=1,c=b/4):c=b/(2*Math.PI)*Math.asin(1/e);return 1>a?-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;f<a.length;f++)a[f].value>e&&(e=a[f].value),a[f].value<h&&(h=a[f].value);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,
+m);k=g/j.steps;x(c,function(){for(var a=0;a<j.steps;a++)if(c.scaleShowLine&&(b.beginPath(),b.arc(q/2,u/2,k*(a+1),0,2*Math.PI,!0),b.strokeStyle=c.scaleLineColor,b.lineWidth=c.scaleLineWidth,b.stroke()),c.scaleShowLabels){b.textAlign="center";b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;var e=j.labels[a];if(c.scaleShowLabelBackdrop){var d=b.measureText(e).width;b.fillStyle=c.scaleBackdropColor;b.beginPath();b.rect(Math.round(q/2-d/2-c.scaleBackdropPaddingX),Math.round(u/2-k*(a+
+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(d+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY));b.fill()}b.textBaseline="middle";b.fillStyle=c.scaleFontColor;b.fillText(e,q/2,u/2-k*(a+1))}},function(e){var d=-Math.PI/2,g=2*Math.PI/a.length,f=1,h=1;c.animation&&(c.animateScale&&(f=e),c.animateRotate&&(h=e));for(e=0;e<a.length;e++)b.beginPath(),b.arc(q/2,u/2,f*v(a[e].value,j,k),d,d+h*g,!1),b.lineTo(q/2,u/2),b.closePath(),b.fillStyle=a[e].color,b.fill(),
+c.segmentShowStroke&&(b.strokeStyle=c.segmentStrokeColor,b.lineWidth=c.segmentStrokeWidth,b.stroke()),d+=h*g},b)},H=function(a,c,b){var e,h,f,d,g,k,j,l,m;a.labels||(a.labels=[]);g=Math.min.apply(Math,[q,u])/2;d=2*c.scaleFontSize;for(e=l=0;e<a.labels.length;e++)b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily,h=b.measureText(a.labels[e]).width,h>l&&(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;f<a.datasets.length;f++)for(m=0;m<a.datasets[f].data.length;m++)a.datasets[f].data[m]>e&&(e=a.datasets[f].data[m]),a.datasets[f].data[m]<h&&(h=a.datasets[f].data[m]);f=Math.floor(l/(0.66*d));d=Math.floor(0.5*(l/d));m=c.scaleShowLabels?c.scaleLabel:null;c.scaleOverride?(j={steps:c.scaleSteps,stepValue:c.scaleStepWidth,graphMin:c.scaleStartValue,labels:[]},z(m,j.labels,j.steps,c.scaleStartValue,c.scaleStepWidth)):j=C(l,f,d,e,h,m);k=g/j.steps;x(c,function(){var e=2*Math.PI/
+a.datasets[0].data.length;b.save();b.translate(q/2,u/2);if(c.angleShowLineOut){b.strokeStyle=c.angleLineColor;b.lineWidth=c.angleLineWidth;for(var d=0;d<a.datasets[0].data.length;d++)b.rotate(e),b.beginPath(),b.moveTo(0,0),b.lineTo(0,-g),b.stroke()}for(d=0;d<j.steps;d++){b.beginPath();if(c.scaleShowLine){b.strokeStyle=c.scaleLineColor;b.lineWidth=c.scaleLineWidth;b.moveTo(0,-k*(d+1));for(var f=0;f<a.datasets[0].data.length;f++)b.rotate(e),b.lineTo(0,-k*(d+1));b.closePath();b.stroke()}c.scaleShowLabels&&
+(b.textAlign="center",b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily,b.textBaseline="middle",c.scaleShowLabelBackdrop&&(f=b.measureText(j.labels[d]).width,b.fillStyle=c.scaleBackdropColor,b.beginPath(),b.rect(Math.round(-f/2-c.scaleBackdropPaddingX),Math.round(-k*(d+1)-0.5*c.scaleFontSize-c.scaleBackdropPaddingY),Math.round(f+2*c.scaleBackdropPaddingX),Math.round(c.scaleFontSize+2*c.scaleBackdropPaddingY)),b.fill()),b.fillStyle=c.scaleFontColor,b.fillText(j.labels[d],0,-k*(d+
+1)))}for(d=0;d<a.labels.length;d++){b.font=c.pointLabelFontStyle+" "+c.pointLabelFontSize+"px "+c.pointLabelFontFamily;b.fillStyle=c.pointLabelFontColor;var f=Math.sin(e*d)*(g+c.pointLabelFontSize),h=Math.cos(e*d)*(g+c.pointLabelFontSize);b.textAlign=e*d==Math.PI||0==e*d?"center":e*d>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;g<a.datasets.length;g++){b.beginPath();
+b.moveTo(0,d*-1*v(a.datasets[g].data[0],j,k));for(var f=1;f<a.datasets[g].data.length;f++)b.rotate(e),b.lineTo(0,d*-1*v(a.datasets[g].data[f],j,k));b.closePath();b.fillStyle=a.datasets[g].fillColor;b.strokeStyle=a.datasets[g].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.fill();b.stroke();if(c.pointDot){b.fillStyle=a.datasets[g].pointColor;b.strokeStyle=a.datasets[g].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(f=0;f<a.datasets[g].data.length;f++)b.rotate(e),b.beginPath(),b.arc(0,d*-1*
+v(a.datasets[g].data[f],j,k),c.pointDotRadius,2*Math.PI,!1),b.fill(),b.stroke()}b.rotate(e)}b.restore()},b)},I=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=0;f<a.length;f++)e+=a[f].value;x(c,null,function(d){var g=-Math.PI/2,f=1,j=1;c.animation&&(c.animateScale&&(f=d),c.animateRotate&&(j=d));for(d=0;d<a.length;d++){var l=j*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,f*h,g,g+l);b.lineTo(q/2,u/2);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&(b.lineWidth=
+c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());g+=l}},b)},J=function(a,c,b){for(var e=0,h=Math.min.apply(Math,[u/2,q/2])-5,f=h*(c.percentageInnerCutout/100),d=0;d<a.length;d++)e+=a[d].value;x(c,null,function(d){var k=-Math.PI/2,j=1,l=1;c.animation&&(c.animateScale&&(j=d),c.animateRotate&&(l=d));for(d=0;d<a.length;d++){var m=l*a[d].value/e*2*Math.PI;b.beginPath();b.arc(q/2,u/2,j*h,k,k+m,!1);b.arc(q/2,u/2,j*f,k+m,k,!0);b.closePath();b.fillStyle=a[d].color;b.fill();c.segmentShowStroke&&
+(b.lineWidth=c.segmentStrokeWidth,b.strokeStyle=c.segmentStrokeColor,b.stroke());k+=m}},b)},K=function(a,c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(s=45,q/a.labels.length<Math.cos(s)*t?(s=90,g-=t):g-=Math.sin(s)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=
+0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(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;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>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();0<s?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<s?(b.translate(n+d*m,p+c.scaleFontSize),b.rotate(-(s*(Math.PI/180))),b.fillText(a.labels[d],
+0,0),b.restore()):b.fillText(a.labels[d],n+d*m,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+d*m,p+3),c.scaleShowGridLines&&0<d?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+d*m,5)):b.lineTo(n+d*m,p+3),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,
+b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){function e(b,c){return p-d*v(a.datasets[b].data[c],j,k)}for(var f=0;f<a.datasets.length;f++){b.strokeStyle=a.datasets[f].strokeColor;b.lineWidth=c.datasetStrokeWidth;b.beginPath();b.moveTo(n,p-d*v(a.datasets[f].data[0],j,k));for(var g=1;g<a.datasets[f].data.length;g++)c.bezierCurve?b.bezierCurveTo(n+m*(g-0.5),e(f,g-1),n+m*(g-0.5),
+e(f,g),n+m*g,e(f,g)):b.lineTo(n+m*g,e(f,g));b.stroke();c.datasetFill?(b.lineTo(n+m*(a.datasets[f].data.length-1),p),b.lineTo(n,p),b.closePath(),b.fillStyle=a.datasets[f].fillColor,b.fill()):b.closePath();if(c.pointDot){b.fillStyle=a.datasets[f].pointColor;b.strokeStyle=a.datasets[f].pointStrokeColor;b.lineWidth=c.pointDotStrokeWidth;for(g=0;g<a.datasets[f].data.length;g++)b.beginPath(),b.arc(n+m*g,p-d*v(a.datasets[f].data[g],j,k),c.pointDotRadius,0,2*Math.PI,!0),b.fill(),b.stroke()}}},b)},L=function(a,
+c,b){var e,h,f,d,g,k,j,l,m,t,r,n,p,s,w=0;g=u;b.font=c.scaleFontStyle+" "+c.scaleFontSize+"px "+c.scaleFontFamily;t=1;for(d=0;d<a.labels.length;d++)e=b.measureText(a.labels[d]).width,t=e>t?e:t;q/a.labels.length<t?(w=45,q/a.labels.length<Math.cos(w)*t?(w=90,g-=t):g-=Math.sin(w)*t):g-=c.scaleFontSize;d=c.scaleFontSize;g=g-5-d;e=Number.MIN_VALUE;h=Number.MAX_VALUE;for(f=0;f<a.datasets.length;f++)for(l=0;l<a.datasets[f].data.length;l++)a.datasets[f].data[l]>e&&(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;e<j.labels.length;e++)h=b.measureText(j.labels[e]).width,d=h>d?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<w?(b.save(),b.textAlign="right"):b.textAlign="center";b.fillStyle=c.scaleFontColor;for(var d=0;d<a.labels.length;d++)b.save(),0<w?(b.translate(n+
+d*m,p+c.scaleFontSize),b.rotate(-(w*(Math.PI/180))),b.fillText(a.labels[d],0,0),b.restore()):b.fillText(a.labels[d],n+d*m+m/2,p+c.scaleFontSize+3),b.beginPath(),b.moveTo(n+(d+1)*m,p+3),b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+(d+1)*m,5),b.stroke();b.lineWidth=c.scaleLineWidth;b.strokeStyle=c.scaleLineColor;b.beginPath();b.moveTo(n,p+5);b.lineTo(n,5);b.stroke();b.textAlign="right";b.textBaseline="middle";for(d=0;d<j.steps;d++)b.beginPath(),b.moveTo(n-3,p-(d+1)*
+k),c.scaleShowGridLines?(b.lineWidth=c.scaleGridLineWidth,b.strokeStyle=c.scaleGridLineColor,b.lineTo(n+r+5,p-(d+1)*k)):b.lineTo(n-0.5,p-(d+1)*k),b.stroke(),c.scaleShowLabels&&b.fillText(j.labels[d],n-8,p-(d+1)*k)},function(d){b.lineWidth=c.barStrokeWidth;for(var e=0;e<a.datasets.length;e++){b.fillStyle=a.datasets[e].fillColor;b.strokeStyle=a.datasets[e].strokeColor;for(var f=0;f<a.datasets[e].data.length;f++){var g=n+c.barValueSpacing+m*f+s*e+c.barDatasetSpacing*e+c.barStrokeWidth*e;b.beginPath();
+b.moveTo(g,p);b.lineTo(g,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p-d*v(a.datasets[e].data[f],j,k)+c.barStrokeWidth/2);b.lineTo(g+s,p);c.barShowStroke&&b.stroke();b.closePath();b.fill()}}},b)},D=window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(a){window.setTimeout(a,1E3/60)},F={}}; \ No newline at end of file
diff --git a/app/assets/javascripts/ci/application.js.coffee b/app/assets/javascripts/ci/application.js.coffee
new file mode 100644
index 00000000000..05aa0f366bb
--- /dev/null
+++ b/app/assets/javascripts/ci/application.js.coffee
@@ -0,0 +1,40 @@
+# This is a manifest file that'll be compiled into application.js, which will include all the files
+# listed below.
+#
+# Any JavaScript/Coffee file within this directory, lib/assets/javascripts, vendor/assets/javascripts,
+# or vendor/assets/javascripts of plugins, if any, can be referenced here using a relative path.
+#
+# It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+# the compiled file.
+#
+# WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
+# GO AFTER THE REQUIRES BELOW.
+#
+#= require pager
+#= require jquery_nested_form
+#= require_tree .
+#
+$(document).on 'click', '.edit-runner-link', (event) ->
+ 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('<i class="fa fa-refresh fa-spin"></i> 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..c30859b484b
--- /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 '<i class="fa fa-refresh fa-spin"/>'
+ @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/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/javascripts/dropzone_input.js.coffee b/app/assets/javascripts/dropzone_input.js.coffee
index a0dcaa8c27a..6f789e668af 100644
--- a/app/assets/javascripts/dropzone_input.js.coffee
+++ b/app/assets/javascripts/dropzone_input.js.coffee
@@ -167,6 +167,7 @@ class @DropzoneInput
dataType: "json"
).success (data) ->
preview.html data.body
+ preview.syntaxHighlight()
renderReferencedUsers data.references.users
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index 176d9cabefa..c4d3e619f5e 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -11,12 +11,13 @@ class @IssuableContext
$(this).submit()
$('.issuable-details').waitForImages ->
+ $('.issuable-affix').on 'affix.bs.affix', ->
+ $(@).width($(@).outerWidth())
+ .on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
+ $(@).width('')
+
$('.issuable-affix').affix offset:
top: ->
@top = ($('.issuable-affix').offset().top - 70)
bottom: ->
@bottom = $('.footer').outerHeight(true)
- $('.issuable-affix').on 'affix.bs.affix', ->
- $(@).width($(@).outerWidth())
- .on 'affixed-top.bs.affix affixed-bottom.bs.affix', ->
- $(@).width('')
diff --git a/app/assets/javascripts/merge_request_tabs.js.coffee b/app/assets/javascripts/merge_request_tabs.js.coffee
index 19a07b6a033..4e56791bde4 100644
--- a/app/assets/javascripts/merge_request_tabs.js.coffee
+++ b/app/assets/javascripts/merge_request_tabs.js.coffee
@@ -66,6 +66,11 @@ class @MergeRequestTabs
@setCurrentAction(action)
+ scrollToElement: (container) ->
+ if window.location.hash
+ top = $(container + " " + window.location.hash).offset().top
+ $('body').scrollTo(top);
+
# Activate a tab based on the current action
activateTab: (action) ->
action = 'notes' if action == 'show'
@@ -122,6 +127,7 @@ class @MergeRequestTabs
document.getElementById('commits').innerHTML = data.html
$('.js-timeago').timeago()
@commitsLoaded = true
+ @scrollToElement(".commits")
loadDiff: (source) ->
return if @diffsLoaded
@@ -131,6 +137,7 @@ class @MergeRequestTabs
success: (data) =>
document.getElementById('diffs').innerHTML = data.html
@diffsLoaded = true
+ @scrollToElement(".diffs")
toggleLoading: ->
$('.mr-loading-status .loading').toggle()
diff --git a/app/assets/javascripts/merge_request_widget.js.coffee b/app/assets/javascripts/merge_request_widget.js.coffee
index 995a2f24093..3176e5a8965 100644
--- a/app/assets/javascripts/merge_request_widget.js.coffee
+++ b/app/assets/javascripts/merge_request_widget.js.coffee
@@ -15,11 +15,12 @@ class @MergeRequestWidget
type: 'GET'
url: $('.merge-request').data('url')
success: (data) =>
- switch data.state
- when 'merged'
- location.reload()
- else
- setTimeout(merge_request_widget.mergeInProgress, 2000)
+ if data.state == "merged"
+ location.reload()
+ else if data.merge_error
+ $('.mr-widget-body').html("<h4>" + data.merge_error + "</h4>")
+ else
+ setTimeout(merge_request_widget.mergeInProgress, 2000)
dataType: 'json'
getMergeStatus: ->
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index 0021d17d85e..4b9f0d68912 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -122,7 +122,9 @@ class @Notes
# or skip if rendered
if @isNewNote(note)
@note_ids.push(note.id)
- $('ul.main-notes-list').append(note.html)
+ $('ul.main-notes-list').
+ append(note.html).
+ syntaxHighlight()
@initTaskList()
###
@@ -275,13 +277,15 @@ class @Notes
Updates the current note field.
###
- updateNote: (xhr, note, status) =>
- note_li = $(".note-row-" + note.id)
- note_li.replaceWith(note.html)
- note_li.find('.note-edit-form').hide()
- note_li.find('.note-body > .note-text').show()
- note_li.find('js-task-list-container').taskList('enable')
- @enableTaskList()
+ updateNote: (_xhr, note, _status) =>
+ # Convert returned HTML to a jQuery object so we can modify it further
+ $html = $(note.html)
+ $html.syntaxHighlight()
+ $html.find('.js-task-list-container').taskList('enable')
+
+ # Find the note's `li` element by ID and replace it with the updated HTML
+ $note_li = $("#note_#{note.id}")
+ $note_li.replaceWith($html)
###
Called in response to clicking the edit note link
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 39a433dfc91..0ea8fffce07 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -24,3 +24,19 @@ class @Project
$.cookie('hide_no_password_message', 'false', { path: path })
$(@).parents('.no-password-message').remove()
e.preventDefault()
+
+ $('.update-notification').on 'click', (e) ->
+ e.preventDefault()
+ notification_level = $(@).data 'notification-level'
+ $('#notification_level').val(notification_level)
+ $('#notification-form').submit()
+ label = null
+ switch notification_level
+ when 0 then label = ' Disabled '
+ when 1 then label = ' Participating '
+ when 2 then label = ' Watching '
+ when 3 then label = ' Global '
+ when 4 then label = ' On Mention '
+ $('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
+ $(@).parents('ul').find('li.active').removeClass 'active'
+ $(@).parent().addClass 'active' \ No newline at end of file
diff --git a/app/assets/javascripts/syntax_highlight.coffee b/app/assets/javascripts/syntax_highlight.coffee
index 510f15d1b49..980f0232d10 100644
--- a/app/assets/javascripts/syntax_highlight.coffee
+++ b/app/assets/javascripts/syntax_highlight.coffee
@@ -1,3 +1,5 @@
+# Syntax Highlighter
+#
# Applies a syntax highlighting color scheme CSS class to any element with the
# `js-syntax-highlight` class
#
@@ -5,5 +7,14 @@
#
# <div class="js-syntax-highlight"></div>
#
+$.fn.syntaxHighlight = ->
+ if $(this).hasClass('js-syntax-highlight')
+ # Given the element itself, apply highlighting
+ $(this).addClass(gon.user_color_scheme)
+ else
+ # Given a parent element, recurse to any of its applicable children
+ $children = $(this).find('.js-syntax-highlight')
+ $children.syntaxHighlight() if $children.length
+
$(document).on 'ready page:load', ->
- $('.js-syntax-highlight').addClass(gon.user_color_scheme)
+ $('.js-syntax-highlight').syntaxHighlight()
diff --git a/app/assets/javascripts/tree.js.coffee b/app/assets/javascripts/tree.js.coffee
index d428db5b422..de8eebcd0b2 100644
--- a/app/assets/javascripts/tree.js.coffee
+++ b/app/assets/javascripts/tree.js.coffee
@@ -16,6 +16,9 @@ class @TreeView
li = $("tr.tree-item")
liSelected = null
$('body').keydown (e) ->
+ if $("input:focus").length > 0 && (e.which == 38 || e.which == 40)
+ return false
+
if e.which is 40
if liSelected
next = liSelected.next()
@@ -38,4 +41,4 @@ class @TreeView
$(liSelected).focus()
else if e.which is 13
path = $('.tree-item.selected .tree-item-file-name a').attr('href')
- Turbolinks.visit(path)
+ if path then Turbolinks.visit(path)
diff --git a/app/assets/javascripts/zen_mode.js.coffee b/app/assets/javascripts/zen_mode.js.coffee
index 8a0564a9098..a1462cf3cae 100644
--- a/app/assets/javascripts/zen_mode.js.coffee
+++ b/app/assets/javascripts/zen_mode.js.coffee
@@ -38,6 +38,8 @@ class @ZenMode
@active_checkbox = $(checkbox)
@active_checkbox.prop('checked', true)
@active_zen_area = @active_checkbox.parent().find('textarea')
+ # Prevent a user-resized textarea from persisting to fullscreen
+ @active_zen_area.removeAttr('style')
@active_zen_area.focus()
exitZenMode: =>
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index 1a5f11df7d1..d9ede637944 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -12,6 +12,7 @@
*/
+@import "base/fonts";
@import "base/variables";
@import "base/mixins";
@import "base/layout";
@@ -60,3 +61,9 @@
* Styles for JS behaviors.
*/
@import "behaviors.scss";
+
+/**
+ * CI specific styles:
+ */
+@import "ci/**/*";
+
diff --git a/app/assets/stylesheets/base/fonts.scss b/app/assets/stylesheets/base/fonts.scss
new file mode 100644
index 00000000000..e214567eca1
--- /dev/null
+++ b/app/assets/stylesheets/base/fonts.scss
@@ -0,0 +1,25 @@
+/* latin-ext */
+@font-face {
+ font-family: 'Source Sans Pro';
+ font-style: normal;
+ font-weight: 300;
+ src: local('Source Sans Pro Light'), local('SourceSansPro-Light'), font-url('SourceSansPro-Light.ttf');
+}
+@font-face {
+ font-family: 'Source Sans Pro';
+ font-style: normal;
+ font-weight: 400;
+ src: local('Source Sans Pro'), local('SourceSansPro-Regular'), font-url('SourceSansPro-Regular.ttf');
+}
+@font-face {
+ font-family: 'Source Sans Pro';
+ font-style: normal;
+ font-weight: 600;
+ src: local('Source Sans Pro Semibold'), local('SourceSansPro-Semibold'), font-url('SourceSansPro-Semibold.ttf');
+}
+@font-face {
+ font-family: 'Source Sans Pro';
+ font-style: normal;
+ font-weight: 700;
+ src: local('Source Sans Pro Bold'), local('SourceSansPro-Bold'), font-url('SourceSansPro-Bold.ttf');
+}
diff --git a/app/assets/stylesheets/base/gl_bootstrap.scss b/app/assets/stylesheets/base/gl_bootstrap.scss
index 21acbfa5e5a..eb8d23d6453 100644
--- a/app/assets/stylesheets/base/gl_bootstrap.scss
+++ b/app/assets/stylesheets/base/gl_bootstrap.scss
@@ -85,14 +85,14 @@
// Labels
.label {
padding: 2px 4px;
- font-size: 12px;
+ font-size: 13px;
font-style: normal;
font-weight: normal;
display: inline-block;
&.label-gray {
- background-color: #eee;
- color: #999;
+ background-color: #f8fafc;
+ color: $gl-gray;
text-shadow: none;
}
@@ -156,10 +156,16 @@
* Add some extra stuff to panels
*
*/
+
+.container-blank .panel .panel-heading {
+ font-size: 17px;
+ line-height: 38px;
+}
+
.panel {
- .panel-heading {
- font-weight: bold;
+ box-shadow: none;
+ .panel-heading {
.panel-head-actions {
position: relative;
top: -5px;
@@ -182,6 +188,10 @@
.pagination {
margin: 0;
}
+
+ .btn {
+ min-width: 124px;
+ }
}
&.panel-small {
@@ -209,6 +219,12 @@
}
}
+.alert-help {
+ background-color: $background-color;
+ border: 1px solid $border-color;
+ color: $gl-gray;
+}
+
// Typography =================================================================
.text-primary,
diff --git a/app/assets/stylesheets/base/gl_variables.scss b/app/assets/stylesheets/base/gl_variables.scss
index 56f4c794e1b..7378d404008 100644
--- a/app/assets/stylesheets/base/gl_variables.scss
+++ b/app/assets/stylesheets/base/gl_variables.scss
@@ -22,6 +22,10 @@ $brand-info: $gl-info;
$brand-warning: $gl-warning;
$brand-danger: $gl-danger;
+$border-radius-base: 3px !default;
+$border-radius-large: 5px !default;
+$border-radius-small: 2px !default;
+
//== Scaffolding
//
@@ -42,17 +46,18 @@ $font-size-base: $gl-font-size;
//
//## Define common padding and border radius sizes and more. Values based on 14px text and 1.428 line-height (~20px to start).
-$padding-base-vertical: 6px;
-$padding-base-horizontal: 14px;
-
+$padding-base-vertical: 9px;
+$padding-base-horizontal: $gl-padding;
+$component-active-color: #fff;
+$component-active-bg: $brand-info;
//== Forms
//
//##
$input-color: $text-color;
-$input-border: #DDD;
-$input-border-focus: $brand-info;
+$input-border: #e7e9ed;
+$input-border-focus: #7F8FA4;
$legend-color: $text-color;
@@ -60,20 +65,20 @@ $legend-color: $text-color;
//
//##
-$pagination-color: #fff;
-$pagination-bg: $brand-success;
+$pagination-color: $gl-gray;
+$pagination-bg: $background-color;
$pagination-border: transparent;
$pagination-hover-color: #fff;
-$pagination-hover-bg: darken($brand-success, 15%);
+$pagination-hover-bg: $brand-info;
$pagination-hover-border: transparent;
$pagination-active-color: #fff;
-$pagination-active-bg: darken($brand-success, 15%);
+$pagination-active-bg: $brand-info;
$pagination-active-border: transparent;
-$pagination-disabled-color: #b4bcc2;
-$pagination-disabled-bg: lighten($brand-success, 15%);
+$pagination-disabled-color: #fff;
+$pagination-disabled-bg: lighten($brand-info, 15%);
$pagination-disabled-border: transparent;
@@ -109,11 +114,12 @@ $alert-border-radius: 0;
//
//##
-$panel-border-radius: 0;
-$panel-default-text: $text-color;
-$panel-default-border: $border-color;
-$panel-default-heading-bg: $background-color;
-
+$panel-border-radius: 2px;
+$panel-default-text: $text-color;
+$panel-default-border: $border-color;
+$panel-default-heading-bg: $background-color;
+$panel-footer-bg: $background-color;
+$panel-inner-border: $border-color;
//== Wells
//
@@ -131,3 +137,22 @@ $code-bg: #f9f2f4;
$kbd-color: #fff;
$kbd-bg: #333;
+
+//== Buttons
+//
+//##
+$btn-default-color: $gl-text-color;
+$btn-default-bg: #fff;
+$btn-default-border: #e7e9ed;
+
+//== Nav
+//
+//##
+$nav-link-padding: 13px $gl-padding;
+
+//== Code
+//
+//##
+$pre-bg: #f8fafc !default;
+$pre-color: $gl-gray !default;
+$pre-border-color: #e7e9ed;
diff --git a/app/assets/stylesheets/base/layout.scss b/app/assets/stylesheets/base/layout.scss
index 734b95e26c0..b91c15d8910 100644
--- a/app/assets/stylesheets/base/layout.scss
+++ b/app/assets/stylesheets/base/layout.scss
@@ -21,7 +21,6 @@ html {
margin-top: 30px;
}
-
.container-limited {
max-width: $fixed-layout-width;
}
diff --git a/app/assets/stylesheets/base/mixins.scss b/app/assets/stylesheets/base/mixins.scss
index 05f5bd79f91..c74a6d39824 100644
--- a/app/assets/stylesheets/base/mixins.scss
+++ b/app/assets/stylesheets/base/mixins.scss
@@ -55,8 +55,11 @@
}
@mixin md-typography {
- font-size: 15px;
- line-height: 1.5;
+ color: $md-text-color;
+
+ a {
+ color: $md-link-color;
+ }
img {
max-width: 100%;
@@ -90,46 +93,88 @@
}
h1 {
- margin-top: 45px;
- font-size: 2.5em;
+ font-size: 1.3em;
+ font-weight: 600;
+ margin: 24px 0 12px 0;
+ padding: 0 0 10px 0;
+ border-bottom: 1px solid #e7e9ed;
+ color: #313236;
}
h2 {
- margin-top: 40px;
- font-size: 2em;
+ font-size: 1.2em;
+ font-weight: 600;
+ margin: 24px 0 12px 0;
+ color: #313236;
}
h3 {
- margin-top: 35px;
- font-size: 1.5em;
+ margin: 24px 0 12px 0;
+ font-size: 1.25em;
}
h4 {
- margin-top: 30px;
- font-size: 1.2em;
+ margin: 24px 0 12px 0;
+ font-size: 1.1em;
+ }
+
+ h5 {
+ margin: 24px 0 12px 0;
+ font-size: 1em;
+ }
+
+ h6 {
+ margin: 24px 0 12px 0;
+ font-size: 0.90em;
}
blockquote {
- color: #888;
+ padding: 8px 21px;
+ margin: 12px 0 12px;
+ border-left: 3px solid #e7e9ed;
+ }
+
+ blockquote p {
+ color: #7f8fa4 !important;
font-size: 15px;
line-height: 1.5;
}
+ p {
+ color:#5c5d5e;
+ margin:6px 0 0 0;
+ }
+
table {
@extend .table;
@extend .table-bordered;
+ margin: 12px 0 12px 0;
+ color: #5c5d5e;
th {
- background: #EEE;
+ background: #f8fafc;
}
}
+ pre {
+ margin: 12px 0 12px 0 !important;
+ background-color: #f8fafc !important;
+ font-size: 13px !important;
+ color: #5b6169 !important;
+ line-height: 1.6em !important;
+ @include border-radius(2px);
+ }
+
p > code {
- font-size: inherit;
font-weight: inherit;
}
+
+ ul {
+ color: #5c5d5e;
+ }
+
li {
- line-height: 1.5;
+ line-height: 1.6em;
}
a[href*="/uploads/"], a[href*="storage.googleapis.com/google-code-attachments/"] {
@@ -149,6 +194,7 @@
}
}
+
@mixin str-truncated($max_width: 82%) {
display: inline-block;
overflow: hidden;
@@ -166,7 +212,7 @@
padding: 0px;
list-style: none;
- li {
+ > li {
padding: 10px 0;
border-bottom: 1px solid #EEE;
overflow: hidden;
@@ -174,13 +220,13 @@
margin: 0px;
&:last-child {
- border:none
+ border-bottom: none;
}
&.active {
background: #f9f9f9;
a {
- font-weight: bold;
+ font-weight: 600;
}
}
@@ -190,8 +236,66 @@
&.light {
a {
- color: #777;
+ color: $gl-gray;
}
}
}
}
+
+@mixin input-big {
+ height: 36px;
+ padding: 5px 10px;
+ font-size: 16px;
+ line-height: 24px;
+ color: #7f8fa4;
+ background-color: #fff;
+ border-color: #e7e9ed;
+}
+
+@mixin btn-big {
+ height: 36px;
+ padding: 5px 10px;
+ font-size: 16px;
+ line-height: 24px;
+}
+
+@mixin nav-menu {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ margin-top: 5px;
+ height: 56px;
+
+ li {
+ display: inline-block;
+
+ a {
+ padding: 14px;
+ font-size: 17px;
+ line-height: 28px;
+ color: #7f8fa4;
+ border-bottom: 2px solid transparent;
+
+ &:hover, &:active, &:focus {
+ text-decoration: none;
+ }
+ }
+
+ &.active a {
+ color: #4c4e54;
+ border-bottom: 2px solid #1cacfc;
+ }
+
+ .badge {
+ font-weight: normal;
+ background-color: #fff;
+ background-color: #eee;
+ color: #78a;
+ }
+ }
+}
+
+.fa-align {
+ top: 20px;
+ position: relative;
+}
diff --git a/app/assets/stylesheets/base/variables.scss b/app/assets/stylesheets/base/variables.scss
index 26d0a1e5363..befd63832d5 100644
--- a/app/assets/stylesheets/base/variables.scss
+++ b/app/assets/stylesheets/base/variables.scss
@@ -1,27 +1,35 @@
-$style_color: #474D57;
$hover: #FFFAF1;
-$gl-text-color: #222222;
-$gl-link-color: #446e9b;
+$gl-text-color: #54565B;
+$gl-text-green: #4A2;
+$gl-text-red: #D12F19;
+$gl-text-orange: #D90;
+$gl-header-color: #4c4e54;
+$gl-link-color: #333c48;
+$md-text-color: #444;
+$md-link-color: #3084bb;
$nprogress-color: #c0392b;
-$gl-font-size: 14px;
+$gl-font-size: 15px;
$list-font-size: 15px;
-$sidebar_collapsed_width: 52px;
+$sidebar_collapsed_width: 62px;
$sidebar_width: 230px;
$avatar_radius: 50%;
$code_font_size: 13px;
$code_line_height: 1.5;
-$border-color: #E5E5E5;
-$background-color: #f5f5f5;
-$header-height: 50px;
+$border-color: #dce0e6;
+$background-color: #F7F8FA;
+$header-height: 58px;
$fixed-layout-width: 1200px;
+$gl-gray: #7f8fa4;
+$gl-padding: 16px;
+$gl-avatar-size: 46px;
/*
* State colors:
*/
$gl-primary: #446e9b;
-$gl-success: #019875;
-$gl-info: #029ACF;
+$gl-success: #44c679;
+$gl-info: #00aaff;
$gl-warning: #EB9532;
$gl-danger: #d9534f;
@@ -35,4 +43,4 @@ $deleted: #f77;
* Fonts
*/
$monospace_font: 'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace;
-$regular_font: "Helvetica Neue", Helvetica, Arial, sans-serif;
+$regular_font: 'Source Sans Pro', "Helvetica Neue", Helvetica, Arial, sans-serif;
diff --git a/app/assets/stylesheets/ci/builds.scss b/app/assets/stylesheets/ci/builds.scss
new file mode 100644
index 00000000000..a27dd0db581
--- /dev/null
+++ b/app/assets/stylesheets/ci/builds.scss
@@ -0,0 +1,76 @@
+.build-page {
+ 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: 250px;
+ z-index: 100;
+
+ a {
+ display: block;
+ margin-bottom: 5px;
+ }
+ }
+
+ .page-sidebar-collapsed {
+ .scroll-controls {
+ left: 70px;
+ }
+ }
+
+ .build-widget {
+ padding: 10px;
+ background: $background-color;
+ margin-bottom: 20px;
+ border-radius: 4px;
+
+ .title {
+ margin-top: 0;
+ color: #666;
+ line-height: 1.5;
+ }
+ .attr-name {
+ color: #777;
+ }
+ }
+
+ .alert-disabled {
+ background: $background-color;
+
+ a {
+ color: #3084bb !important;
+ }
+ }
+
+ .build-top-menu {
+ margin-top: 0;
+ margin-bottom: 2px;
+ }
+}
+
diff --git a/app/assets/stylesheets/ci/lint.scss b/app/assets/stylesheets/ci/lint.scss
new file mode 100644
index 00000000000..6d2bd33b28b
--- /dev/null
+++ b/app/assets/stylesheets/ci/lint.scss
@@ -0,0 +1,10 @@
+.ci-body {
+ .incorrect-syntax{
+ font-size: 19px;
+ color: red;
+ }
+ .correct-syntax{
+ font-size: 19px;
+ color: #47a447;
+ }
+}
diff --git a/app/assets/stylesheets/ci/projects.scss b/app/assets/stylesheets/ci/projects.scss
new file mode 100644
index 00000000000..8c5273abcda
--- /dev/null
+++ b/app/assets/stylesheets/ci/projects.scss
@@ -0,0 +1,59 @@
+.ci-body {
+ .project-title {
+ margin: 0;
+ color: #444;
+ font-size: 20px;
+ line-height: 1.5;
+ }
+
+ .wide-table-holder {
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ }
+
+ .builds,
+ .projects-table {
+ .light {
+ border-color: $border-color;
+ }
+
+ th, td {
+ padding: 10px $gl-padding;
+ }
+
+ td {
+ color: $gl-gray;
+ vertical-align: middle !important;
+
+ a {
+ font-weight: normal;
+ text-decoration: none;
+ }
+ }
+ }
+
+ .commit-info {
+ .attr-name {
+ margin-right: 5px;
+ }
+
+ pre.commit-message {
+ background: none;
+ padding: 0;
+ margin: 0;
+ border: none;
+ margin: 20px 0;
+ border-radius: 0;
+ }
+ }
+
+ .loading{
+ font-size: 20px;
+ }
+
+ .ci-charts {
+ fieldset {
+ margin-bottom: 16px;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/ci/runners.scss b/app/assets/stylesheets/ci/runners.scss
new file mode 100644
index 00000000000..2b15ab83129
--- /dev/null
+++ b/app/assets/stylesheets/ci/runners.scss
@@ -0,0 +1,36 @@
+.ci-body {
+ .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/status.scss b/app/assets/stylesheets/ci/status.scss
new file mode 100644
index 00000000000..a7d3b2197f1
--- /dev/null
+++ b/app/assets/stylesheets/ci/status.scss
@@ -0,0 +1,37 @@
+.ci-status {
+ padding: 2px 7px;
+ margin-right: 5px;
+ border: 1px solid #EEE;
+ white-space: nowrap;
+ @include border-radius(4px);
+
+ &:hover {
+ text-decoration: none;
+ }
+
+ &.ci-failed {
+ color: $gl-danger;
+ border-color: $gl-danger;
+ }
+
+ &.ci-success {
+ color: $gl-success;
+ border-color: $gl-success;
+ }
+
+ &.ci-info {
+ color: $gl-info;
+ border-color: $gl-info;
+ }
+
+ &.ci-disabled {
+ color: $gl-gray;
+ border-color: $gl-gray;
+ }
+
+ &.ci-pending,
+ &.ci-running {
+ color: $gl-warning;
+ border-color: $gl-warning;
+ }
+}
diff --git a/app/assets/stylesheets/ci/xterm.scss b/app/assets/stylesheets/ci/xterm.scss
new file mode 100644
index 00000000000..532dede0b23
--- /dev/null
+++ b/app/assets/stylesheets/ci/xterm.scss
@@ -0,0 +1,906 @@
+.ci-body {
+ // 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/generic/avatar.scss b/app/assets/stylesheets/generic/avatar.scss
index 8595887c3b9..36e582d4854 100644
--- a/app/assets/stylesheets/generic/avatar.scss
+++ b/app/assets/stylesheets/generic/avatar.scss
@@ -23,8 +23,13 @@
&.s24 { width: 24px; height: 24px; margin-right: 8px; }
&.s26 { width: 26px; height: 26px; margin-right: 8px; }
&.s32 { width: 32px; height: 32px; margin-right: 10px; }
+ &.s36 { width: 36px; height: 36px; margin-right: 10px; }
+ &.s46 { width: 46px; height: 46px; margin-right: 15px; }
+ &.s48 { width: 48px; height: 48px; margin-right: 10px; }
&.s60 { width: 60px; height: 60px; margin-right: 12px; }
&.s90 { width: 90px; height: 90px; margin-right: 15px; }
+ &.s110 { width: 110px; height: 110px; margin-right: 15px; }
+ &.s140 { width: 140px; height: 140px; margin-right: 20px; }
&.s160 { width: 160px; height: 160px; margin-right: 20px; }
}
@@ -38,5 +43,7 @@
&.s32 { font-size: 22px; line-height: 32px; }
&.s60 { font-size: 32px; line-height: 60px; }
&.s90 { font-size: 36px; line-height: 90px; }
- &.s160 { font-size: 96px; line-height: 1.33; }
+ &.s110 { font-size: 40px; line-height: 112px; font-weight: 300; }
+ &.s140 { font-size: 72px; line-height: 140px; }
+ &.s160 { font-size: 96px; line-height: 160px; }
}
diff --git a/app/assets/stylesheets/generic/blocks.scss b/app/assets/stylesheets/generic/blocks.scss
index 3536a68f416..6ce34b5c3e8 100644
--- a/app/assets/stylesheets/generic/blocks.scss
+++ b/app/assets/stylesheets/generic/blocks.scss
@@ -1,19 +1,62 @@
.light-well {
- background: #f9f9f9;
+ background-color: #f8fafc;
padding: 15px;
}
.centered-light-block {
text-align: center;
- color: #888;
+ color: $gl-gray;
margin: 20px;
}
.nothing-here-block {
text-align: center;
padding: 20px;
- color: #666;
+ color: $gl-gray;
font-weight: normal;
font-size: 16px;
line-height: 36px;
}
+
+.gray-content-block {
+ margin: -$gl-padding;
+ background-color: $background-color;
+ padding: $gl-padding;
+ margin-bottom: 0px;
+ border-top: 1px solid $border-color;
+ border-bottom: 1px solid $border-color;
+ color: $gl-gray;
+
+ &.top-block {
+ border-top: none;
+ }
+
+ &.middle-block {
+ margin-top: 0;
+ margin-bottom: 0;
+ }
+
+ &.clear-block {
+ margin-bottom: $gl-padding - 1px;
+ padding-bottom: $gl-padding;
+ }
+
+ &.second-block {
+ margin-top: -1px;
+ margin-bottom: 0;
+ }
+
+ &.footer-block {
+ margin-top: 0;
+ border-bottom: none;
+ margin-bottom: -$gl-padding;
+ }
+
+ .title {
+ color: $gl-text-color;
+ }
+
+ .oneline {
+ line-height: 42px;
+ }
+}
diff --git a/app/assets/stylesheets/generic/buttons.scss b/app/assets/stylesheets/generic/buttons.scss
index cd6bf64c0ae..cf76f538e01 100644
--- a/app/assets/stylesheets/generic/buttons.scss
+++ b/app/assets/stylesheets/generic/buttons.scss
@@ -1,3 +1,6 @@
+body {
+ text-rendering: geometricPrecision;
+}
.btn {
@extend .btn-default;
@@ -10,7 +13,7 @@
}
&.btn-save {
- @extend .btn-primary;
+ @extend .btn-success;
}
&.btn-remove {
@@ -72,3 +75,154 @@
}
}
}
+
+.btn-group-next {
+ .btn {
+ padding: 9px 0px;
+ font-size: 15px;
+ color: #7f8fa4;
+ border-color: #e7e9ed;
+ width: 140px;
+
+ &.active {
+ border-color: $gl-info;
+ background: $gl-info;
+ color: #fff;
+ }
+ }
+}
+
+@mixin btn-info {
+ @include border-radius(2px);
+
+ border-width: 1px;
+ border-style: solid;
+ text-transform: uppercase;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 18px;
+ padding: 11px 16px;
+ letter-spacing: .4px;
+
+ &:hover {
+ border-width: 1px;
+ border-style: solid;
+ }
+
+ &:focus {
+ border-width: 1px;
+ border-style: solid;
+ }
+
+ &:active {
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ border-width: 1px;
+ border-style: solid;
+ }
+}
+
+@mixin btn-middle {
+ @include border-radius(2px);
+
+ border-width: 1px;
+ border-style: solid;
+ text-transform: uppercase;
+ font-size: 13px;
+ font-weight: 600;
+ line-height: 18px;
+ padding: 11px 24px;
+ letter-spacing: .4px;
+
+ &:hover {
+ border-width: 1px;
+ border-style: solid;
+ }
+
+ &:focus {
+ border-width: 1px;
+ border-style: solid;
+ }
+
+ &:active {
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ border-width: 1px;
+ border-style: solid;
+ }
+}
+
+
+@mixin btn-green {
+ background-color: #28b061;
+ border: 1px solid #26a65c;
+ color: #fff;
+
+ &:hover {
+ background-color: #26ab5d;
+ border: 1px solid #229954;
+ color: #fff;
+ }
+
+ &:focus {
+ background-color: #26ab5d;
+ border: 1px solid #229954;
+ color: #fff;
+ }
+
+ &:active {
+ @include box-shadow (inset 0 0 4px rgba(0, 0, 0, 0.12));
+
+ background-color: #23a158 !important;
+ border: 1px solid #229954 !important;
+ color: #fff !important;
+ }
+}
+
+/*Butons*/
+
+@mixin bnt-project {
+ background-color: #f0f2f5;
+ border-color: #dce0e5;
+ color: #313236;
+
+ &:hover {
+ border-color:#dce0e5;
+ background-color: #ebeef2;
+ color: #313236;
+ }
+
+ &:focus {
+ border-color: #dce0e5;
+ background-color: #ebeef2;
+ color: #313236;
+ }
+
+ &:active {
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+
+ color: #313236 !important;
+ border-color: #c6cacf !important;
+ background-color: #e4e7ed !important;
+ }
+}
+
+@mixin btn-remove {
+ background-color: #f72e60;
+ border-color: #ee295a;
+
+ &:hover {
+ background-color: #e82757;
+ border-color: #e32555;
+ }
+
+ &:focus {
+ background-color: #e82757;
+ border-color: #e32555;
+ }
+
+ &:active {
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+ background-color: #d42450 !important;
+ border-color: #e12554 !important;
+ }
+
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/generic/callout.scss b/app/assets/stylesheets/generic/callout.scss
new file mode 100644
index 00000000000..f1699d21c9b
--- /dev/null
+++ b/app/assets/stylesheets/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/generic/common.scss b/app/assets/stylesheets/generic/common.scss
index e5902597c4d..45e284542d2 100644
--- a/app/assets/stylesheets/generic/common.scss
+++ b/app/assets/stylesheets/generic/common.scss
@@ -1,12 +1,13 @@
/** COLORS **/
-.cgray { color: gray }
+.cgray { color: $gl-gray; }
.clgray { color: #BBB }
-.cred { color: #D12F19 }
-.cgreen { color: #4a2 }
+.cred { color: $gl-text-red; }
+.cgreen { color: $gl-text-green; }
.cdark { color: #444 }
/** COMMON CLASSES **/
.prepend-top-10 { margin-top:10px }
+.prepend-top-default { margin-top: $gl-padding; }
.prepend-top-20 { margin-top:20px }
.prepend-left-10 { margin-left:10px }
.prepend-left-20 { margin-left:20px }
@@ -20,10 +21,10 @@
.underlined-link { text-decoration: underline; }
.hint { font-style: italic; color: #999; }
-.light { color: #888 }
+.light { color: $gl-gray; }
.slead {
- color: #666;
+ color: $gl-gray;
font-size: 15px;
margin-bottom: 12px;
font-weight: normal;
@@ -74,8 +75,6 @@ pre {
color: $gl-link-color;
}
-.help li { color:$style_color; }
-
.back-link {
font-size: 14px;
}
@@ -303,7 +302,7 @@ table {
}
.btn-sign-in {
- margin-top: 7px;
+ margin-top: 8px;
text-shadow: none;
}
@@ -314,7 +313,7 @@ table {
}
.wiki .highlight, .note-body .highlight {
- margin-bottom: 9px;
+ margin: 12px 0 12px 0;
}
.wiki .code {
@@ -355,14 +354,14 @@ table {
}
.description {
- font-size: 16px;
+ font-size: $gl-font-size;
color: #666;
margin-top: 8px;
}
}
.profiler-results {
- top: 50px !important;
+ top: 73px !important;
.profiler-button,
.profiler-controls {
@@ -371,21 +370,27 @@ table {
}
.center-top-menu {
- list-style: none;
+ @include nav-menu;
text-align: center;
margin-top: 5px;
- padding-bottom: 15px;
- margin-bottom: 15px;
-
- li {
- display: inline-block;
+ margin-bottom: $gl-padding;
+ height: 56px;
+ margin-top: -$gl-padding;
+ padding-top: $gl-padding;
- a {
- padding: 15px;
- }
+ &.no-bottom {
+ margin-bottom: 0;
+ }
- &.active a {
- color: #666;
- }
+ &.no-top {
+ margin-top: 0;
}
}
+
+.dropzone .dz-preview .dz-progress {
+ border-color: $border-color !important;
+}
+
+.dropzone .dz-preview .dz-progress .dz-upload {
+ background: $gl-success !important;
+}
diff --git a/app/assets/stylesheets/generic/files.scss b/app/assets/stylesheets/generic/files.scss
index f845342c67b..9dd77747884 100644
--- a/app/assets/stylesheets/generic/files.scss
+++ b/app/assets/stylesheets/generic/files.scss
@@ -3,7 +3,11 @@
*
*/
.file-holder {
- border: 1px solid $border-color;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ border: none;
+ border-top: 1px solid #E7E9EE;
+ border-bottom: 1px solid #E7E9EE;
margin-bottom: 1em;
table {
@@ -49,7 +53,7 @@
}
&.wiki {
- padding: 25px;
+ padding: $gl-padding;
.highlight {
margin-bottom: 9px;
@@ -90,7 +94,7 @@
border-right: none;
}
background: #fff;
- padding: 8px;
+ padding: 10px $gl-padding;
}
.lines {
pre {
@@ -100,6 +104,33 @@
border: none;
}
}
+ img.avatar {
+ border: 0 none;
+ float: none;
+ margin: 0;
+ padding: 0;
+ }
+ td.blame-commit {
+ background: #f9f9f9;
+ min-width: 350px;
+
+ .commit-author-link {
+ color: #888;
+ }
+ }
+ td.blame-numbers {
+ pre {
+ color: #AAA;
+ white-space: pre;
+ }
+ background: #f1f1f1;
+ border-left: 1px solid #DDD;
+ }
+ td.lines {
+ code {
+ font-family: $monospace_font;
+ }
+ }
}
&.logs {
diff --git a/app/assets/stylesheets/generic/filters.scss b/app/assets/stylesheets/generic/filters.scss
index bd93a79722d..8e6922c9231 100644
--- a/app/assets/stylesheets/generic/filters.scss
+++ b/app/assets/stylesheets/generic/filters.scss
@@ -2,31 +2,6 @@
margin-right: 15px;
}
-.issues-state-filters {
- li.active a {
- border-color: #DDD !important;
-
- &, &:hover, &:active, &.active {
- background: #f5f5f5 !important;
- border-bottom: 1px solid #f5f5f5 !important;
- }
- }
-}
-
-.issues-details-filters {
- font-size: 13px;
- background: #f5f5f5;
- margin: -10px 0;
- padding: 10px 15px;
- margin-top: -15px;
- border-left: 1px solid #DDD;
- border-right: 1px solid #DDD;
-
- .btn {
- font-size: 13px;
- }
-}
-
@media (min-width: 800px) {
.issues-filters,
.issues_bulk_update {
diff --git a/app/assets/stylesheets/generic/header.scss b/app/assets/stylesheets/generic/header.scss
index 6a29b32e196..543ce41ab52 100644
--- a/app/assets/stylesheets/generic/header.scss
+++ b/app/assets/stylesheets/generic/header.scss
@@ -24,29 +24,27 @@ header {
z-index: 100;
margin-bottom: 0;
min-height: $header-height;
+ background-color: #fff;
border: none;
- border-bottom: 1px solid #EEE;
.container-fluid {
- background: #FFF;
width: 100% !important;
filter: none;
+ padding: 0;
.nav > li > a {
- color: #888;
- font-size: 14px;
+ color: #7f8fa4;
+ font-size: 18px;
padding: 0;
- background-color: #f5f5f5;
margin: ($header-height - 28) / 2 0;
margin-left: 10px;
- border-radius: 40px;
height: 28px;
width: 28px;
line-height: 28px;
text-align: center;
&:hover, &:focus, &:active {
- background-color: #EEE;
+ background-color: #FFF;
}
}
@@ -56,6 +54,7 @@ header {
border-radius: 0;
position: absolute;
right: 2px;
+ top: 15px;
&:hover {
background-color: #EEE;
@@ -70,16 +69,16 @@ header {
.title {
margin: 0;
overflow: hidden;
- font-size: 18px;
+ font-size: 19px;
line-height: $header-height;
- font-weight: bold;
- color: #444;
+ font-weight: normal;
+ color: #4c4e54;
text-overflow: ellipsis;
vertical-align: top;
white-space: nowrap;
a {
- color: #444;
+ color: #4c4e54;
&:hover {
text-decoration: underline;
}
@@ -94,7 +93,7 @@ header {
.search {
margin-right: 10px;
margin-left: 10px;
- margin-top: ($header-height - 28) / 2;
+ margin-top: ($header-height - 36) / 2;
form {
margin: 0;
@@ -105,13 +104,8 @@ header {
width: 220px;
background-image: image-url("icon-search.png");
background-repeat: no-repeat;
- background-position: 10px;
- height: inherit;
- padding: 4px 6px;
- padding-left: 25px;
- font-size: 13px;
- background-color: #f5f5f5;
- border-color: #f5f5f5;
+ background-position: 195px;
+ @include input-big;
&:focus {
@include box-shadow(none);
diff --git a/app/assets/stylesheets/generic/issue_box.scss b/app/assets/stylesheets/generic/issue_box.scss
index 869e586839b..b1fb87a6830 100644
--- a/app/assets/stylesheets/generic/issue_box.scss
+++ b/app/assets/stylesheets/generic/issue_box.scss
@@ -5,10 +5,13 @@
*/
.issue-box {
+ @include border-radius(3px);
+
display: inline-block;
- padding: 4px 13px;
+ padding: 10px $gl-padding;
font-weight: normal;
- margin-right: 5px;
+ margin-right: 10px;
+ font-size: $gl-font-size;
&.issue-box-closed {
background-color: $gl-danger;
@@ -21,7 +24,7 @@
}
&.issue-box-open {
- background-color: $gl-success;
+ background-color: #019875;
color: #FFF;
}
diff --git a/app/assets/stylesheets/generic/lists.scss b/app/assets/stylesheets/generic/lists.scss
index 4b7ff84de2b..3bfed8de772 100644
--- a/app/assets/stylesheets/generic/lists.scss
+++ b/app/assets/stylesheets/generic/lists.scss
@@ -49,8 +49,6 @@
}
}
- .author { color: #999; }
-
.list-item-name {
float: left;
position: relative;
@@ -71,15 +69,6 @@
font-size: $list-font-size;
line-height: 18px;
}
-
- .row_title {
- color: $gray-dark;
-
- &:hover {
- color: $text-color;
- text-decoration: underline;
- }
- }
}
}
@@ -109,3 +98,28 @@ ul.bordered-list {
li.task-list-item {
list-style-type: none;
}
+
+ul.content-list {
+ @include basic-list;
+
+ margin: 0;
+ padding: 0;
+
+ > li {
+ padding: $gl-padding;
+ border-color: #f1f2f4;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ color: $gl-gray;
+
+ .avatar {
+ margin-right: 15px;
+ }
+
+ .controls {
+ padding-top: 10px;
+ float: right;
+ }
+ }
+}
+
diff --git a/app/assets/stylesheets/generic/markdown_area.scss b/app/assets/stylesheets/generic/markdown_area.scss
index a4fc82e90bf..ed0333d2336 100644
--- a/app/assets/stylesheets/generic/markdown_area.scss
+++ b/app/assets/stylesheets/generic/markdown_area.scss
@@ -65,8 +65,11 @@
position: relative;
}
-.md-header ul {
- float: left;
+.md-header {
+ ul {
+ float: left;
+ margin-bottom: 1px;
+ }
}
.referenced-users {
@@ -80,7 +83,7 @@
.md-preview-holder {
background: #FFF;
border: 1px solid #ddd;
- min-height: 100px;
+ min-height: 169px;
padding: 5px;
box-shadow: none;
}
@@ -105,7 +108,7 @@
.markdown-area {
background: #FFF;
border: 1px solid #ddd;
- min-height: 100px;
+ min-height: 140px;
padding: 5px;
box-shadow: none;
width: 100%;
diff --git a/app/assets/stylesheets/generic/mobile.scss b/app/assets/stylesheets/generic/mobile.scss
index bb7b9356c70..36ae126f865 100644
--- a/app/assets/stylesheets/generic/mobile.scss
+++ b/app/assets/stylesheets/generic/mobile.scss
@@ -80,6 +80,23 @@
%ul.notes .note-role, .note-actions {
display: none;
}
+
+ .center-top-menu {
+ height: 45px;
+
+ li a {
+ font-size: 14px;
+ padding: 19px 10px;
+ }
+ }
+
+ .projects-search-form {
+ margin: 0 -5px !important;
+
+ .btn {
+ display: none;
+ }
+ }
}
@media (max-width: $screen-sm-max) {
diff --git a/app/assets/stylesheets/generic/pagination.scss b/app/assets/stylesheets/generic/pagination.scss
new file mode 100644
index 00000000000..6677f94dafd
--- /dev/null
+++ b/app/assets/stylesheets/generic/pagination.scss
@@ -0,0 +1,34 @@
+.gl-pagination {
+ border-top: 1px solid $border-color;
+ background-color: $background-color;
+ margin: -$gl-padding;
+ margin-top: 0;
+
+ .pagination {
+ padding: 0;
+ margin: 0;
+ display: block;
+
+ li.first,
+ li.last,
+ li.next,
+ li.prev {
+ > a {
+ color: $link-color;
+
+ &:hover {
+ color: #fff;
+ }
+ }
+ }
+
+ li > a,
+ li > span {
+ border: none;
+ margin: 0;
+ @include border-radius(0 !important);
+ padding: 13px 19px;
+ border-right: 1px solid $border-color;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/generic/selects.scss b/app/assets/stylesheets/generic/selects.scss
index d8e0dc028d1..f0860de1c49 100644
--- a/app/assets/stylesheets/generic/selects.scss
+++ b/app/assets/stylesheets/generic/selects.scss
@@ -3,9 +3,9 @@
.select2-choice {
background: #FFF;
border-color: #DDD;
- height: 34px;
- padding: 6px 14px;
- font-size: 14px;
+ height: 42px;
+ padding: 8px $gl-padding;
+ font-size: $gl-font-size;
line-height: 1.42857143;
@include border-radius(4px);
@@ -13,7 +13,7 @@
.select2-arrow {
background: #FFF;
border-left: none;
- padding-top: 3px;
+ padding-top: 5px;
}
}
}
diff --git a/app/assets/stylesheets/generic/sidebar.scss b/app/assets/stylesheets/generic/sidebar.scss
index 320bdb1c765..c5ea3aca7ca 100644
--- a/app/assets/stylesheets/generic/sidebar.scss
+++ b/app/assets/stylesheets/generic/sidebar.scss
@@ -18,14 +18,27 @@
}
.content-wrapper {
+ min-height: 100vh;
width: 100%;
padding: 20px;
- background: #FFF;
+ background: #EAEBEC;
+
+ .container-fluid {
+ background: #FFF;
+ padding: $gl-padding;
+ min-height: 90vh;
+
+ &.container-blank {
+ background: none;
+ padding: 0;
+ border: none;
+ }
+ }
}
.nav-sidebar {
- margin-top: 29 + $header-height;
- margin-bottom: 50px;
+ margin-top: 14 + $header-height;
+ margin-bottom: 100px;
transition-duration: .3s;
list-style: none;
overflow: hidden;
@@ -43,13 +56,14 @@
}
a {
- padding: 8px 15px;
- font-size: 13px;
- line-height: 18px;
+ padding: 7px 15px;
+ font-size: $gl-font-size;
+ line-height: 24px;
color: $gray;
display: block;
text-decoration: none;
- padding-left: 16px;
+ padding-left: 22px;
+ font-weight: normal;
&:hover {
text-decoration: none;
@@ -60,9 +74,9 @@
}
i {
- width: 20px;
+ width: 16px;
color: $gray-light;
- margin-right: 23px;
+ margin-right: 13px;
}
.count {
@@ -108,41 +122,59 @@
}
@mixin folded-sidebar {
- padding-left: 50px;
+ padding-left: 60px;
transition-duration: .3s;
.sidebar-wrapper {
width: $sidebar_collapsed_width;
+ .header-logo {
+ width: $sidebar_collapsed_width;
+
+ a {
+ padding-left: 12px;
+
+ .gitlab-text-container {
+ display: none;
+ }
+ }
+ }
+
.nav-sidebar {
width: $sidebar_collapsed_width;
li a {
- padding-left: 16px;
+ span {
+ display: none;
+ }
}
}
.collapse-nav a {
- left: 0px;
width: $sidebar_collapsed_width;
}
.sidebar-user {
+ padding-left: 12px;
width: $sidebar_collapsed_width;
+
+ .username {
+ display: none;
+ }
}
}
}
.collapse-nav a {
+ width: $sidebar_width;
position: fixed;
- top: $header-height;
- left: 198px;
+ bottom: 0;
+ left: 0;
font-size: 13px;
background: transparent;
- width: 32px;
- height: 28px;
+ height: 40px;
text-align: center;
- line-height: 28px;
+ line-height: 40px;
transition-duration: .3s;
}
@@ -176,16 +208,18 @@
}
.sidebar-user {
+ padding: 9px 22px;
position: fixed;
- bottom: 0;
+ bottom: 40px;
width: $sidebar_width;
- padding: 10px;
overflow: hidden;
transition-duration: .3s;
.username {
- margin-top: 5px;
+ margin-left: 10px;
width: $sidebar_width - 2 * 10px;
+ font-size: 16px;
+ line-height: 34px;
}
}
@@ -202,7 +236,7 @@
float: left;
height: $header-height;
width: 100%;
- padding: ($header-height - 36 ) / 2 8px;
+ padding: 10px 22px;
overflow: hidden;
img {
@@ -219,8 +253,8 @@
float: left;
margin: 0;
margin-left: 14px;
- font-size: 18px;
- line-height: $header-height - 14;
+ font-size: 19px;
+ line-height: 41px;
font-weight: normal;
}
}
diff --git a/app/assets/stylesheets/generic/timeline.scss b/app/assets/stylesheets/generic/timeline.scss
index 97831eb7c27..74bbaabad39 100644
--- a/app/assets/stylesheets/generic/timeline.scss
+++ b/app/assets/stylesheets/generic/timeline.scss
@@ -1,119 +1,50 @@
.timeline {
- list-style: none;
- padding: 20px 0 20px;
- position: relative;
+ @include basic-list;
- &:before {
- top: 0;
- bottom: 0;
- position: absolute;
- content: " ";
- width: 3px;
- background-color: #eeeeee;
- margin-left: 29px;
- }
+ margin: 0;
+ padding: 0;
.timeline-entry {
- position: relative;
- margin-top: 5px;
- margin-left: 30px;
- margin-bottom: 10px;
- clear: both;
-
-
- &:target {
- .timeline-entry-inner .timeline-content {
- -webkit-animation:target-note 2s linear;
- background: $hover;
- }
+ padding: $gl-padding;
+ border-color: #f1f2f4;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ color: $gl-gray;
+ border-bottom: 1px solid #f1f2f4;
+ border-right: 1px solid #f1f2f4;
+
+ &:last-child {
+ border-bottom: none;
}
- .timeline-entry-inner {
- position: relative;
- margin-left: -20px;
-
- &:before, &:after {
- content: " ";
- display: table;
- }
-
- .timeline-icon {
- margin-top: 2px;
- background: #fff;
- color: #737881;
- float: left;
- @include border-radius($avatar_radius);
- @include box-shadow(0 0 0 3px #EEE);
- overflow: hidden;
-
- .avatar {
- margin: 0;
- padding: 0;
- }
- }
-
- .timeline-content {
- position: relative;
- background: $background-color;
- padding: 10px 15px;
- margin-left: 60px;
-
- img {
- max-width: 100%;
- }
+ .avatar {
+ margin-right: 15px;
+ }
- &:after {
- content: '';
- display: block;
- position: absolute;
- width: 0;
- height: 0;
- border-style: solid;
- border-width: 9px 9px 9px 0;
- border-color: transparent $background-color transparent transparent;
- left: 0;
- top: 10px;
- margin-left: -9px;
- }
- }
+ .controls {
+ padding-top: 10px;
+ float: right;
}
}
- .system-note .timeline-entry-inner {
- .timeline-icon {
- background: none;
- margin-left: 12px;
- margin-top: 0;
- @include box-shadow(none);
-
- span {
- margin: 0 2px;
- font-size: 16px;
- color: #eeeeee;
- }
+ .note-text {
+ p:last-child {
+ margin-bottom: 0;
}
+ }
- .timeline-content {
- background: none;
- margin-left: 45px;
- padding: 0px 15px;
-
- &:after { border: 0; }
-
- .note-header {
- span { font-size: 12px; }
-
- .avatar {
- margin-right: 5px;
- }
- }
-
- .note-text {
- font-size: 12px;
- margin-left: 20px;
- }
+ .system-note {
+ .note-text {
+ color: $gl-gray !important;
}
}
+
+ .diff-file {
+ border: 1px solid $border-color;
+ border-bottom: none;
+ margin-left: 0;
+ margin-right: 0;
+ }
}
@media (max-width: $screen-xs-max) {
@@ -132,3 +63,8 @@
}
}
}
+
+.discussion .timeline-entry {
+ margin: 0;
+ border-right: none;
+}
diff --git a/app/assets/stylesheets/generic/typography.scss b/app/assets/stylesheets/generic/typography.scss
index 34b4ee3e17e..6a3cb49baae 100644
--- a/app/assets/stylesheets/generic/typography.scss
+++ b/app/assets/stylesheets/generic/typography.scss
@@ -2,11 +2,29 @@
* Headers
*
*/
+body {
+ text-rendering:optimizeLegibility;
+ -webkit-text-shadow: rgba(255,255,255,0.01) 0 0 1px;
+}
+
.page-title {
margin-top: 0px;
- line-height: 1.5;
- font-weight: normal;
- margin-bottom: 5px;
+ line-height: 1.3;
+ font-size: 1.25em;
+ font-weight: 600;
+}
+
+.page-title-empty {
+ margin-top: 0px;
+ line-height: 1.3;
+ font-size: 1.25em;
+ font-weight: 600;
+ margin: 12px 7px 12px 7px;
+}
+
+h1, h2, h3, h4, h5, h6 {
+ color: $gl-header-color;
+ font-weight: 500;
}
/** CODE **/
@@ -50,6 +68,7 @@ a > code {
@include md-typography;
word-wrap: break-word;
+ padding: 7px;
/* Link to current header. */
h1, h2, h3, h4, h5, h6 {
@@ -78,12 +97,19 @@ a > code {
}
}
- ul {
+ ul,ol {
padding: 0;
- margin: 0 0 9px 25px !important;
+ margin: 6px 0 6px 18px !important;
+ }
+ ol {
+ color: #5c5d5e;
}
}
+.md-area {
+ @include md-typography;
+}
+
.md {
@include md-typography;
}
@@ -96,6 +122,9 @@ textarea.js-gfm-input {
font-family: $monospace_font;
}
+.md-preview {
+}
+
.strikethrough {
text-decoration: line-through;
-}
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/generic/zen.scss b/app/assets/stylesheets/generic/zen.scss
index 7e86a0fe4b9..32e2c020e06 100644
--- a/app/assets/stylesheets/generic/zen.scss
+++ b/app/assets/stylesheets/generic/zen.scss
@@ -4,7 +4,7 @@
}
.zen-enter-link {
- color: #888;
+ color: $gl-gray;
position: absolute;
top: 0px;
right: 4px;
@@ -13,7 +13,7 @@
.zen-leave-link {
display: none;
- color: #888;
+ color: $gl-text-color;
position: absolute;
top: 10px;
right: 10px;
diff --git a/app/assets/stylesheets/highlight/white.scss b/app/assets/stylesheets/highlight/white.scss
index e0edfb80b42..20a144ef952 100644
--- a/app/assets/stylesheets/highlight/white.scss
+++ b/app/assets/stylesheets/highlight/white.scss
@@ -1,19 +1,24 @@
/* https://github.com/aahan/pygments-github-style */
pre.code.highlight.white,
.code.white {
+ background-color: #f8fafc;
+ font-size: 13px;
+ color: #5b6169;
+ line-height: 1.6em;
- background-color: #fff;
- color: #333;
-
- pre.highlight,
.line-numbers,
.line-numbers a {
+ background-color: $background-color !important;
+ color: $gl-gray !important;
+ }
+
+ pre.highlight {
background-color: #fff !important;
color: #333 !important;
}
pre.code {
- border-left: 1px solid #bbb;
+ border-left: 1px solid $border-color;
}
// highlight line via anchor
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index e7125c03993..fbd7c363de1 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -26,14 +26,6 @@
margin-top: 10px;
}
-.commit-stat-summary {
- color: #666;
- font-size: 14px;
- font-weight: normal;
- padding: 3px 0;
- margin-bottom: 10px;
-}
-
.commit-info-row {
margin-bottom: 10px;
.avatar {
@@ -47,11 +39,6 @@
}
.commit-box {
- margin: 10px 0;
- border-top: 1px solid #ddd;
- border-bottom: 1px solid #ddd;
- padding: 20px 0;
-
.commit-title {
margin: 0;
}
@@ -61,35 +48,34 @@
}
}
-.file-stats a {
- color: $style_color;
-}
-
.file-stats {
+ ul {
+ list-style: none;
+ margin: 0;
+ padding: 10px 0;
+
+ li {
+ padding: 3px 0px;
+ }
+ }
.new-file {
a {
- color: #090;
- }
- i {
- color: #1BCF00;
+ color: $gl-text-green;
}
}
.renamed-file {
- i {
- color: #FE9300;
+ a {
+ color: $gl-text-orange;
}
}
.deleted-file {
a {
- color: #B00;
- }
- i {
- color: #EE0000;
+ color: $gl-text-red;
}
}
.edit-file{
- i{
- color: #555;
+ a {
+ color: $gl-text-color;
}
}
}
@@ -121,3 +107,16 @@
z-index: 2;
}
}
+
+.commit-ci-menu {
+ padding: 0;
+ margin: 0;
+ list-style: none;
+ margin-top: 5px;
+ height: 56px;
+ margin: -16px;
+ padding: 16px;
+ text-align: center;
+ margin-top: 0px;
+ margin-bottom: 2px;
+}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 359f4073e87..de2ae93df37 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -52,7 +52,7 @@ li.commit {
}
.commit-row-message {
- color: #444;
+ color: $gl-link-color;
&:hover {
text-decoration: underline;
@@ -88,12 +88,12 @@ li.commit {
}
.commit-row-info {
- color: #777;
+ color: $gl-gray;
line-height: 24px;
font-size: 13px;
a {
- color: #777;
+ color: $gl-gray;
}
.committed_ago {
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index c1103a1c2e6..25a86cd0f94 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -2,7 +2,7 @@
.side {
.panel {
.panel-heading {
- background: #EEE;
+ background: $background-color;
border-top-left-radius: 0;
}
border-top-left-radius: 0;
@@ -38,11 +38,11 @@
float: left;
.avatar {
- @include border-radius(0px);
+ @include border-radius(50%);
}
.identicon {
- line-height: 40px;
+ line-height: 46px;
}
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 1557c243db5..5e7e59a6af8 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -1,12 +1,14 @@
.diff-file {
- border: 1px solid $border-color;
- margin-bottom: 1em;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ border: none;
+ border-bottom: 1px solid #E7E9EE;
.diff-header {
position: relative;
background: $background-color;
border-bottom: 1px solid $border-color;
- padding: 10px 15px;
+ padding: 10px 16px;
color: #555;
z-index: 10;
@@ -45,7 +47,7 @@
overflow-y: hidden;
background: #FFF;
color: #333;
- font-size: $code_font_size;
+
.old {
span.idiff {
background-color: #f8cbcb;
@@ -82,7 +84,7 @@
border: none;
margin: 0px;
padding: 0px;
- td {
+ .line_holder td {
line-height: $code_line_height;
font-size: $code_font_size;
}
@@ -367,3 +369,7 @@
white-space: pre-wrap;
}
+.inline-parallel-buttons {
+ float: right;
+ margin-top: -5px;
+}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 759ba6b1c22..1d565477dd4 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -9,6 +9,10 @@
width: 100%;
}
+ .ace_gutter-cell {
+ background-color: $background-color;
+ }
+
.cancel-btn {
color: #B94A48;
&:hover {
@@ -32,14 +36,12 @@
.file-title {
@extend .monospace;
- font-size: 14px;
- padding: 5px;
}
.editor-ref {
background: $background-color;
padding: 11px 15px;
- border-right: 1px solid #CCC;
+ border-right: 1px solid $border-color;
display: inline-block;
margin: -5px -5px;
margin-right: 10px;
@@ -50,5 +52,15 @@
display: inline-block;
width: 200px;
}
+
+ .form-control {
+ margin-top: -3px;
+ }
+ }
+
+ .form-actions {
+ margin: -$gl-padding;
+ margin-top: 0;
+ padding: $gl-padding
}
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index d4af7506d5b..ca2ee455423 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -1,70 +1,58 @@
/**
- * Events labels
- *
- */
-.event_label {
- &.pushed {
- padding: 0 2px;
- }
-
- &.opened {
- padding: 0 2px;
- }
-
- &.closed {
- padding: 0 2px;
- }
-
- &.merged {
- padding: 0 2px;
- }
-
- &.left,
- &.joined {
- padding: 0 2px;
- float: none;
- }
-}
-
-/**
* Dashboard events feed
*
*/
.event-item {
- &:first-child {
- padding-top: 0;
- }
+ font-size: $gl-font-size;
+ padding: $gl-padding;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ border-bottom: 1px solid #f1f2f4;
+ color: #7f8fa4;
&.event-inline {
.avatar {
position: relative;
top: -2px;
}
+
+ .event-title {
+ line-height: 44px;
+ }
+
+ .event-item-timestamp {
+ line-height: 44px;
+ }
+ }
+
+ a {
+ color: #4c4e54;
+ }
+
+ .avatar {
+ margin-right: 15px;
}
- padding: 12px 0px;
- border-bottom: 1px solid #eee;
.event-title {
- max-width: 70%;
@include str-truncated(calc(100% - 174px));
- font-weight: 500;
- font-size: 14px;
+ font-weight: 600;
+
.author_name {
color: #333;
}
}
+
.event-body {
- font-size: 13px;
- margin-left: 35px;
+ margin-left: 63px;
margin-right: 80px;
- color: #777;
.event-note {
margin-top: 5px;
word-wrap: break-word;
.md {
- font-size: 13px;
+ color: #7f8fa4;
+ font-size: $gl-font-size;
iframe.twitter-share-button {
vertical-align: bottom;
@@ -94,7 +82,7 @@
.event-note-icon {
color: #777;
float: left;
- font-size: 16px;
+ font-size: $gl-font-size;
line-height: 16px;
margin-right: 5px;
}
@@ -116,7 +104,7 @@
&:last-child { border:none }
.event_commits {
- margin-top: 5px;
+ margin-top: 9px;
li {
&.commit {
@@ -125,10 +113,12 @@
padding-left: 0;
border: none;
.commit-row-title {
- font-size: 12px;
+ font-size: $gl-font-size;
}
}
+
&.commits-stat {
+ margin-top: 3px;
display: block;
padding: 3px;
padding-left: 0;
@@ -142,7 +132,6 @@
.event-item-timestamp {
float: right;
- color: #999;
line-height: 22px;
}
}
@@ -186,12 +175,3 @@
}
}
}
-
-.event_filter {
- li a {
- font-size: 13px;
- padding: 5px 10px;
- background: $background-color;
- margin-left: 4px;
- }
-}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 2b1b747139a..07a38a19fad 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -10,3 +10,9 @@
.milestone-row {
@include str-truncated(90%);
}
+
+.dashboard .side .panel .panel-heading .input-group {
+ .form-control {
+ height: 42px;
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 586e7b5f8da..b5c61f7f91d 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -25,8 +25,6 @@
}
.issuable-context-title {
- font-size: 14px;
- line-height: 1.4;
margin-bottom: 5px;
.avatar {
@@ -34,14 +32,50 @@
}
label {
- color: #666;
+ color: $gl-gray;
font-weight: normal;
margin-right: 4px;
}
}
-.issuable-affix .context {
- font-size: 13px;
+.project-issuable-filter {
+ .controls {
+ float: right;
+ margin-top: 7px;
+ }
+
+ .center-top-menu {
+ text-align: left;
+ }
+}
- .btn { font-size: 13px; }
+.issuable-details {
+ .page-title {
+ margin-top: -15px;
+ padding: 10px 0;
+ margin-bottom: 0;
+ color: $gl-gray;
+ font-size: 16px;
+
+ .author {
+ color: $gl-gray;
+ }
+
+ .issue-id {
+ font-size: 19px;
+ color: $gl-text-color;
+ }
+ }
+
+ .issue-title {
+ margin: 0;
+ }
+
+ .description {
+ margin-top: 6px;
+
+ p:last-child {
+ margin-bottom: 0;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3572f33e91f..4bf58cb4a59 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,6 +1,6 @@
.issues-list {
.issue {
- padding: 10px 15px;
+ padding: 10px $gl-padding;
position: relative;
.issue-title {
@@ -10,8 +10,7 @@
}
.issue-info {
- color: #999;
- font-size: 13px;
+ color: $gl-gray;
}
.issue-check {
@@ -47,10 +46,6 @@
}
}
-.participants {
- margin-bottom: 20px;
-}
-
.issue-search-form {
margin: 0;
height: 24px;
@@ -137,11 +132,6 @@ form.edit-issue {
}
}
-h2.issue-title {
- margin-top: 0;
- font-weight: bold;
-}
-
.issue-form .select2-container {
width: 250px !important;
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 10fce5b3daa..d8c8e5ad0a4 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -3,10 +3,10 @@
*
*/
.mr-state-widget {
- background: #FAFAFA;
+ background: #f8fafc;
margin-bottom: 20px;
- color: #666;
- border: 1px solid #e5e5e5;
+ color: $gl-gray;
+ border: 1px solid #eef0f2;
@include box-shadow(0 1px 1px rgba(0, 0, 0, 0.05));
@include border-radius(3px);
@@ -29,6 +29,14 @@
padding: 5px;
line-height: 20px;
+ &.right {
+ float: right;
+ padding-top: 12px;
+ a {
+ color: $gl-gray;
+ }
+ }
+
.remove_source_checkbox {
margin: 0;
}
@@ -36,7 +44,7 @@
}
.ci_widget {
- border-bottom: 1px solid #EEE;
+ border-bottom: 1px solid #eef0f2;
i {
margin-right: 4px;
@@ -89,20 +97,14 @@
}
}
-@media(min-width: $screen-sm-max) {
- .merge-request .merge-request-tabs{
- li {
- a {
- padding: 15px 40px;
- font-size: 14px;
- }
- }
- }
-}
-
.merge-request .merge-request-tabs{
- margin-top: 30px;
- margin-bottom: 20px;
+ @include nav-menu;
+ margin: -$gl-padding;
+ padding: $gl-padding;
+ text-align: center;
+ border-top: 1px solid #e7e9ed;
+ margin-top: 18px;
+ margin-bottom: 3px;
}
.mr_source_commit,
@@ -136,8 +138,7 @@
}
.merge-request-info {
- color: #999;
- font-size: 13px;
+ color: $gl-gray;
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 15e3948e402..e80dc9e84a1 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -6,4 +6,8 @@ li.milestone {
h4 {
font-weight: bold;
}
+
+ .progress {
+ height: 6px;
+ }
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 203f9374cee..fdc2c3332df 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -72,9 +72,13 @@
.common-note-form {
margin: 0;
- background: #F9F9F9;
- padding: 5px;
- border: 1px solid #DDD;
+ background: #f8fafc;
+ padding: $gl-padding;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+ border-right: 1px solid #f1f2f4;
+ border-top: 1px solid #f1f2f4;
+ margin-bottom: -$gl-padding;
}
.note-form-actions {
@@ -105,7 +109,7 @@
.note-edit-form {
display: none;
- font-size: 13px;
+ font-size: 15px;
.form-actions {
padding-left: 20px;
@@ -142,9 +146,9 @@
}
.discussion-reply-holder {
- background: #f9f9f9;
+ background: $background-color;
padding: 10px 15px;
- border-top: 1px solid #DDD;
+ border-top: 1px solid $border-color;
}
}
@@ -166,6 +170,6 @@
background: #FFF;
padding: 5px;
margin-top: -11px;
- border: 1px solid #DDD;
+ border: 1px solid $border-color;
font-size: 13px;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 85c828ec1ad..2a77f065aed 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -14,6 +14,19 @@ ul.notes {
margin: 0px;
padding: 0px;
+ .system-note {
+ font-size: 14px;
+ padding-top: 10px;
+ padding-bottom: 10px;
+ background: #f8fafc;
+
+ .timeline-icon {
+ .avatar {
+ visibility: hidden;
+ }
+ }
+ }
+
.discussion-header,
.note-header {
@extend .cgray;
@@ -34,10 +47,8 @@ ul.notes {
content: "\00b7";
}
- font-size: 13px;
-
a {
- @extend .cgray;
+ color: $gl-gray;
&:hover {
text-decoration: underline;
@@ -45,8 +56,9 @@ ul.notes {
}
}
.author {
- color: #333;
- font-weight: bold;
+ color: #4c4e54;
+ margin-right: 3px;
+
&:hover {
color: $gl-link-color;
}
@@ -59,7 +71,7 @@ ul.notes {
margin-top: 1px;
border: 1px solid #bbb;
background-color: transparent;
- color: #999;
+ color: $gl-gray;
}
}
@@ -133,8 +145,6 @@ ul.notes {
}
.diff-file .notes_holder {
- font-size: 13px;
- line-height: 18px;
font-family: $regular_font;
td {
@@ -176,8 +186,7 @@ ul.notes {
a {
margin-left: 5px;
-
- color: #999;
+ color: $gl-gray;
i.fa {
font-size: 16px;
@@ -226,8 +235,6 @@ ul.notes {
filter: alpha(opacity=0);
&:hover {
- width: 38px;
- font-size: 20px;
background: $gl-info;
color: #FFF;
@include show-add-diff-note;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 488dded549e..818aa10aefe 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -1,3 +1,14 @@
+.alert_holder {
+ margin: -16px;
+
+ .alert-link {
+ font-weight: normal;
+ }
+}
+.no-ssh-key-message {
+ background-color: #f28d35;
+ margin-bottom: 16px;
+}
.new_project,
.edit_project {
fieldset.features {
@@ -16,9 +27,13 @@
.project-home-panel {
text-align: center;
+ background: #f7f8fa;
+ margin: -$gl-padding;
+ padding: $gl-padding;
+ padding: 44px 0 17px 0;
.project-identicon-holder {
- margin-bottom: 15px;
+ margin-bottom: 16px;
.avatar, .identicon {
margin: 0 auto;
@@ -36,22 +51,27 @@
.project-home-desc {
h1 {
+ color: #313236;
margin: 0;
- margin-bottom: 10px;
- font-size: 26px;
- font-weight: bold;
+ margin-bottom: 6px;
+ font-size: 23px;
+ font-weight: normal;
}
p {
- font-size: 18px;
- color: #666;
- display: inline;
+ color: #5c5d5e;
}
}
.git-clone-holder {
- max-width: 600px;
- margin: 20px auto;
+ max-width: 498px;
+
+ .form-control {
+ background: #FFF;
+ font-size: 14px;
+ height: 42px;
+ margin-left: -1px;
+ }
}
.visibility-level-label {
@@ -60,29 +80,37 @@
color: inherit;
}
}
+ .input-group {
+ display: inline-table;
+ position: relative;
+ top: 17px;
+ margin-bottom: 44px;
+ }
.project-repo-buttons {
- margin-top: 25px;
- margin-bottom: 25px;
+ margin-top: 12px;
+ margin-bottom: 0px;
.btn {
- @extend .btn-info;
-
- margin-left: 10px;
- font-weight: bold;
- font-size: 14px;
- line-height: 16px;
- padding: 8px 12px;
+ @include bnt-project;
+ @include btn-info;
.count {
- padding-left: 7px;
display: inline-block;
- margin-left: 7px;
}
}
}
}
+.split-one {
+ display: inline-table;
+ margin-right: 12px;
+
+ a {
+ margin: -1px !important;
+ }
+}
+
.git-clone-holder {
.project-home-dropdown + & {
margin-right: 45px;
@@ -92,11 +120,11 @@
cursor: auto;
@extend .monospace;
background: #FAFAFA;
- width: 100%;
+ width: 101%;
}
.input-group-addon {
- background: #FAFAFA;
+ background: #f7f8fa;
&.git-protocols {
padding: 0;
@@ -104,11 +132,120 @@
.input-group-btn:last-child > .btn {
@include border-radius-right(0);
+
+ border-left: 1px solid #c6cacf;
+ margin-left: -2px !important;
}
}
}
}
+.projects-search-form {
+
+ .input-group .form-control {
+ height: 42px;
+ }
+}
+
+.input-group-btn {
+ .btn {
+ @include bnt-project;
+ @include btn-middle;
+
+ &:hover {
+ outline: none;
+ }
+
+ &:focus {
+ outline: none;
+ }
+
+ &:active {
+ outline: none;
+ }
+ }
+
+ .active {
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+
+ border: 1px solid #c6cacf !important;
+ background-color: #e4e7ed !important;
+ }
+
+ .btn-green {
+ @include btn-green
+ }
+
+}
+
+.split-repo-buttons {
+ display: inline-table;
+ margin: 0 12px 0 12px;
+
+ .btn{
+ @include bnt-project;
+ @include btn-info;
+ }
+
+ .dropdown-toggle {
+ margin: -5px;
+ }
+}
+
+#notification-form {
+ margin-left: 5px;
+}
+
+.dropdown-new {
+ margin-left: -5px;
+}
+
+.open > .dropdown-new.btn {
+ @include box-shadow(inset 0 0 4px rgba(0, 0, 0, 0.12));
+
+ border: 1px solid #c6cacf !important;
+ background-color: #e4e7ed !important;
+ text-transform: uppercase;
+ color: #313236 !important;
+ font-size: 13px;
+ font-weight: 600;
+}
+
+.dropdown-menu {
+ @include box-shadow(rgba(76, 86, 103, 0.247059) 0px 0px 1px 0px, rgba(31, 37, 50, 0.317647) 0px 2px 18px 0px);
+ @include border-radius (0px);
+
+ border: none;
+ padding: 16px 0;
+ font-size: 14px;
+ font-weight: 100;
+
+ li a {
+ color: #5f697a;
+ line-height: 30px;
+
+ &:hover {
+ background-color: #3084bb !important;
+ }
+ }
+
+ .fa-fw {
+ margin-right: 8px;
+ }
+}
+
+.fa-bell {
+ margin-right: 6px;
+}
+
+.fa-angle-down {
+ margin-left: 6px;
+}
+
+.project-home-panel .project-home-dropdown {
+ margin: 13px 0px 0;
+}
+
.project-visibility-level-holder {
.radio {
margin-bottom: 10px;
@@ -184,10 +321,10 @@ ul.nav.nav-projects-tabs {
.breadcrumb.repo-breadcrumb {
padding: 0;
- line-height: 34px;
- background: white;
+ line-height: 42px;
+ background: transparent;
border: none;
- font-size: 16px;
+ margin: 0;
> li + li:before {
padding: 0 3px;
@@ -196,26 +333,18 @@ ul.nav.nav-projects-tabs {
}
.fork-namespaces {
- .thumbnail {
-
- &.fork-exists-thumbnail {
- border-color: #EEE;
+ .fork-thumbnail {
+ text-align: center;
+ margin-bottom: $gl-padding;
- .caption {
- color: #999;
- }
+ .caption {
+ padding: $gl-padding 0;
+ min-height: 30px;
}
- &.fork-thumbnail {
- border-color: #AAA;
-
- &:hover {
- background-color: $hover;
- }
- }
-
- a {
- text-decoration: none;
+ img {
+ @include border-radius(50%);
+ max-width: 100px;
}
}
}
@@ -233,17 +362,43 @@ table.table.protected-branches-list tr.no-border {
.project-stats {
text-align: center;
+ margin-top: 15px;
+ margin-bottom: 0;
+ padding-top: 10px;
+ padding-bottom: 4px;
+
+ ul.nav-pills {
+ display:inline-block;
+ }
+
+ .nav-pills li {
+ display:inline;
+ }
+
+ .nav > li > a {
+ @include btn-info;
+ @include bnt-project;
- ul.nav-pills { display:inline-block; }
- li { display:inline; }
- a { float:left; }
+ background-color: transparent;
+ border: 1px solid #f7f8fa;
+ margin-left: 12px;
+ }
+
+ li {
+ display:inline;
+ }
+
+ a {
+ float:left;
+ font-size: 17px;
+ }
li.missing a {
- color: #bbb;
- border: 1px dashed #ccc;
+ color: #5a6069;
+ border: 1px dashed #dce0e5;
&:hover {
- background-color: #FAFAFA;
+ background-color: #f0f2f5;
}
}
}
@@ -253,41 +408,106 @@ pre.light-well {
}
.projects-search-form {
- max-width: 600px;
- margin: 0 auto;
- margin-bottom: 20px;
+ margin: -$gl-padding;
+ background-color: #f8fafc;
+ padding: $gl-padding;
+ margin-bottom: 0px;
+ border-top: 1px solid #e7e9ed;
+ border-bottom: 1px solid #e7e9ed;
+}
+
+.git-empty {
+ margin: 0 7px 0 7px;
+
+ h5 {
+ color: #5c5d5e;
+ }
+
+ .light-well {
+ @include border-radius (2px);
+
+ color: #5b6169;
+ font-size: 13px;
+ line-height: 1.6em;
+ }
+}
+
+.project-footer {
+ margin-top: 20px;
+
+ .btn-remove {
+ @include btn-middle;
+ @include btn-remove;
- input {
- border-color: #BBB;
+ float: left !important;
}
}
/*
* Projects list rendered on dashboard and user page
*/
+
.projects-list {
@include basic-list;
.project-row {
+ padding: $gl-padding;
+ border-color: #f1f2f4;
+ margin-left: -$gl-padding;
+ margin-right: -$gl-padding;
+
+ &.no-description {
+ .project {
+ line-height: 44px;
+ }
+ }
+
.project-full-name {
@include str-truncated;
- font-weight: bold;
- font-size: 15px;
+ font-weight: 600;
+ color: #4c4e54;
+ }
+
+ .project-controls {
+ float: right;
+ color: $gl-gray;
+ line-height: 45px;
+ color: #7f8fa4;
+
+ a:hover {
+ text-decoration: none;
+ }
}
.project-description {
- color: #888;
- font-size: 13px;
+ color: #7f8fa4;
p {
@include str-truncated;
margin-bottom: 0;
- color: #888;
+ color: #7f8fa4;
}
}
}
+
+ .bottom {
+ padding-top: $gl-padding;
+ padding-bottom: 0;
+ }
}
.panel .projects-list li {
padding: 10px 15px;
+ margin: 0;
+}
+
+.project-show-activity {
+ .activity-filter-block {
+ margin-top: -1px;
+ }
}
+
+.inline-form {
+ display: inline-block;
+}
+
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 81e2aa7bb9c..271cc547e2b 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -63,51 +63,21 @@
padding-right: 8px;
.commit-author-name {
- color: gray;
+ color: $gl-gray;
}
}
.tree_commit {
- color: gray;
+ color: $gl-gray;
.tree-commit-link {
- color: gray;
+ color: $gl-gray;
&:hover {
text-decoration: underline;
}
}
}
-
- .blame {
- img.avatar {
- border: 0 none;
- float: none;
- margin: 0;
- padding: 0;
- }
- td.blame-commit {
- background: #f9f9f9;
- min-width: 350px;
-
- .commit-author-link {
- color: #888;
- }
- }
- td.blame-numbers {
- pre {
- color: #AAA;
- white-space: pre;
- }
- background: #f1f1f1;
- border-left: 1px solid #DDD;
- }
- td.lines {
- code {
- font-family: $monospace_font;
- }
- }
- }
}
.tree-ref-holder {
@@ -132,20 +102,30 @@
list-style: none;
margin: 0;
padding: 0;
- margin-bottom: 10px;
+ margin-bottom: 5px;
.commit {
- padding: 10px 15px;
+ padding: $gl-padding 0;
.commit-row-title {
- font-size: 13px;
-
.commit-row-message {
font-weight: normal;
- color: #555;
}
}
}
}
#modal-remove-blob > .modal-dialog { width: 850px; }
+
+.blob-upload-dropzone-previews {
+ text-align: center;
+ border: 2px;
+ border-style: dashed;
+ border-color: $border-color;
+ min-height: 200px;
+}
+
+.upload-link {
+ font-weight: normal;
+ color: $md-link-color;
+}
diff --git a/app/assets/stylesheets/themes/gitlab-theme.scss b/app/assets/stylesheets/themes/gitlab-theme.scss
index 77b62c3153f..8d9a0aae568 100644
--- a/app/assets/stylesheets/themes/gitlab-theme.scss
+++ b/app/assets/stylesheets/themes/gitlab-theme.scss
@@ -9,15 +9,19 @@
@mixin gitlab-theme($color-light, $color, $color-darker, $color-dark) {
.page-with-sidebar {
.header-logo {
- background-color: $color-darker;
- border-color: $color-darker;
+ background-color: $color;
+ border-color: $color;
a {
color: $color-light;
+
+ h3 {
+ color: $color-light;
+ }
}
&:hover {
- background-color: $color-dark;
+ background-color: $color-darker;
a {
color: #FFF;
}
@@ -83,7 +87,7 @@
}
$theme-blue: #2980B9;
-$theme-charcoal: #474D57;
+$theme-charcoal: #333c47;
$theme-graphite: #888888;
$theme-gray: #373737;
$theme-green: #019875;
@@ -95,7 +99,7 @@ body {
}
&.ui_charcoal {
- @include gitlab-theme(#979DA7, $theme-charcoal, #373D47, #24272D);
+ @include gitlab-theme(#c5d0de, $theme-charcoal, #2b333d, #24272D);
}
&.ui_graphite {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index f38e07af84b..7c134d2ec9b 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -46,6 +46,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:gravatar_enabled,
:twitter_sharing_enabled,
:sign_in_text,
+ :help_page_text,
:home_page_url,
:after_sign_out_path,
:max_attachment_size,
diff --git a/app/controllers/admin/labels_controller.rb b/app/controllers/admin/labels_controller.rb
new file mode 100644
index 00000000000..3b070e65d0d
--- /dev/null
+++ b/app/controllers/admin/labels_controller.rb
@@ -0,0 +1,58 @@
+class Admin::LabelsController < Admin::ApplicationController
+ before_action :set_label, only: [:show, :edit, :update, :destroy]
+
+ def index
+ @labels = Label.templates.page(params[:page]).per(PER_PAGE)
+ end
+
+ def show
+ end
+
+ def new
+ @label = Label.new
+ end
+
+ def edit
+ end
+
+ def create
+ @label = Label.new(label_params)
+ @label.template = true
+
+ if @label.save
+ redirect_to admin_labels_url, notice: "Label was created"
+ else
+ render :new
+ end
+ end
+
+ def update
+ if @label.update(label_params)
+ redirect_to admin_labels_path, notice: 'label was successfully updated.'
+ else
+ render :edit
+ end
+ end
+
+ def destroy
+ @label.destroy
+ @labels = Label.templates
+
+ respond_to do |format|
+ format.html do
+ redirect_to(admin_labels_path, notice: 'Label was removed')
+ end
+ format.js
+ end
+ end
+
+ private
+
+ def set_label
+ @label = Label.find(params[:id])
+ end
+
+ def label_params
+ params[:label].permit(:title, :color)
+ end
+end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index 6092c79c254..00f41a10dd1 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -56,13 +56,19 @@ class Admin::UsersController < Admin::ApplicationController
end
def confirm
- if user.confirm!
+ if user.confirm
redirect_to :back, notice: "Successfully confirmed"
else
redirect_to :back, alert: "Error occurred. User was not confirmed"
end
end
+ def login_as
+ sign_in(user)
+ flash[:alert] = "Logged in as #{user.username}"
+ redirect_to root_path
+ end
+
def disable_two_factor
user.disable_two_factor!
redirect_to admin_user_path(user),
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index cb1cf13d34d..527c9da0faa 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -1,4 +1,5 @@
require 'gon'
+require 'fogbugz'
class ApplicationController < ActionController::Base
include Gitlab::CurrentSettings
@@ -20,7 +21,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings
- helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :git_import_enabled?
+ helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -116,9 +117,14 @@ class ApplicationController < ActionController::Base
redirect_to request.original_url.gsub(/\.git\Z/, '') and return
end
- @project = Project.find_with_namespace("#{namespace}/#{id}")
+ project_path = "#{namespace}/#{id}"
+ @project = Project.find_with_namespace(project_path)
+
if @project and can?(current_user, :read_project, @project)
+ if @project.path_with_namespace != project_path
+ redirect_to request.original_url.gsub(project_path, @project.path_with_namespace) and return
+ end
@project
elsif current_user.nil?
@project = nil
@@ -133,9 +139,6 @@ class ApplicationController < ActionController::Base
def repository
@repository ||= project.repository
- rescue Grit::NoSuchPathError => e
- log_exception(e)
- nil
end
def authorize_project!(action)
@@ -337,6 +340,10 @@ class ApplicationController < ActionController::Base
current_application_settings.import_sources.include?('google_code')
end
+ def fogbugz_import_enabled?
+ current_application_settings.import_sources.include?('fogbugz')
+ end
+
def git_import_enabled?
current_application_settings.import_sources.include?('git')
end
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 904d26a39f4..202e9da9eee 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -32,6 +32,7 @@ class AutocompleteController < ApplicationController
@users ||= User.none
@users = @users.search(params[:search]) if params[:search].present?
@users = @users.active
+ @users = @users.reorder(:name)
@users = @users.page(params[:page]).per(PER_PAGE)
unless params[:search].present?
diff --git a/app/controllers/ci/admin/application_controller.rb b/app/controllers/ci/admin/application_controller.rb
new file mode 100644
index 00000000000..4ec2dc9c2cf
--- /dev/null
+++ b/app/controllers/ci/admin/application_controller.rb
@@ -0,0 +1,10 @@
+module Ci
+ module Admin
+ class ApplicationController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :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..38abfdeafbf
--- /dev/null
+++ b/app/controllers/ci/admin/builds_controller.rb
@@ -0,0 +1,18 @@
+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)
+
+ @builds =
+ case @scope
+ when "pending"
+ @builds.pending
+ when "running"
+ @builds.running
+ else
+ @builds
+ 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..9a68add9083
--- /dev/null
+++ b/app/controllers/ci/admin/runners_controller.rb
@@ -0,0 +1,72 @@
+module Ci
+ class Admin::RunnersController < Ci::Admin::ApplicationController
+ before_action :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
+ if params[:search].present?
+ @gl_projects = ::Project.search(params[:search])
+ @projects = @projects.where(gitlab_id: @gl_projects.select(:id))
+ end
+ @projects = @projects.where("ci_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..9be470660e6
--- /dev/null
+++ b/app/controllers/ci/application_controller.rb
@@ -0,0 +1,74 @@
+module Ci
+ class ApplicationController < ::ApplicationController
+ def self.railtie_helpers_paths
+ "app/helpers/ci"
+ end
+
+ helper_method :gl_project
+
+ private
+
+ def authenticate_public_page!
+ unless project.public
+ authenticate_user!
+
+ return access_denied! unless can?(current_user, :read_project, gl_project)
+ end
+ end
+
+ def authenticate_token!
+ unless project.valid_token?(params[:token])
+ return head(403)
+ end
+ end
+
+ def authorize_access_project!
+ unless can?(current_user, :read_project, gl_project)
+ return page_404
+ end
+ end
+
+ def authorize_manage_builds!
+ unless can?(current_user, :manage_builds, gl_project)
+ return page_404
+ end
+ end
+
+ def authenticate_admin!
+ return render_404 unless current_user.is_admin?
+ end
+
+ def authorize_manage_project!
+ unless can?(current_user, :admin_project, gl_project)
+ return page_404
+ end
+ end
+
+ def page_404
+ render file: "#{Rails.root}/public/404.html", status: 404, layout: false
+ 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 gl_project
+ ::Project.find(@project.gitlab_id)
+ 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..b0b8b62fced
--- /dev/null
+++ b/app/controllers/ci/builds_controller.rb
@@ -0,0 +1,52 @@
+module Ci
+ class BuildsController < Ci::ApplicationController
+ before_action :authenticate_user!, except: [:status]
+ before_action :project
+ before_action :authorize_access_project!, except: [:status]
+ before_action :authorize_manage_project!, except: [:status, :retry, :cancel]
+ before_action :authorize_manage_builds!, only: [:retry, :cancel]
+ before_action :build
+
+ 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 build_path(build)
+ end
+ end
+
+ def status
+ render json: @build.to_json(only: [:status, :id, :sha, :coverage], methods: :sha)
+ end
+
+ def cancel
+ @build.cancel
+
+ redirect_to build_path(@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
+
+ def build_path(build)
+ namespace_project_build_path(build.gl_project.namespace, build.gl_project, build)
+ 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..7e6705c9702
--- /dev/null
+++ b/app/controllers/ci/commits_controller.rb
@@ -0,0 +1,32 @@
+module Ci
+ class CommitsController < Ci::ApplicationController
+ before_action :authenticate_user!, except: [:status, :show]
+ before_action :authenticate_public_page!, only: :show
+ before_action :project
+ before_action :authorize_access_project!, except: [:status, :show, :cancel]
+ before_action :authorize_manage_builds!, only: [:cancel]
+
+ def status
+ commit = Ci::Project.find(params[:project_id]).commits.find_by_sha!(params[: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 namespace_project_commit_path(commit.gl_project.namespace, commit.gl_project, 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!(params[: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..89b784a1e89
--- /dev/null
+++ b/app/controllers/ci/events_controller.rb
@@ -0,0 +1,21 @@
+module Ci
+ class EventsController < Ci::ApplicationController
+ EVENTS_PER_PAGE = 50
+
+ before_action :authenticate_user!
+ before_action :project
+ before_action :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/lints_controller.rb b/app/controllers/ci/lints_controller.rb
new file mode 100644
index 00000000000..24dd1b5c93a
--- /dev/null
+++ b/app/controllers/ci/lints_controller.rb
@@ -0,0 +1,26 @@
+module Ci
+ class LintsController < Ci::ApplicationController
+ before_action :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
+ @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..adb913a783f
--- /dev/null
+++ b/app/controllers/ci/projects_controller.rb
@@ -0,0 +1,56 @@
+module Ci
+ class ProjectsController < Ci::ApplicationController
+ before_action :authenticate_user!, except: [:build, :badge, :show]
+ before_action :authenticate_public_page!, only: :show
+ before_action :project, only: [:build, :show, :badge, :toggle_shared_runners, :dumped_yaml]
+ before_action :authorize_access_project!, except: [:build, :badge, :show, :new]
+ before_action :authorize_manage_project!, only: [:toggle_shared_runners, :dumped_yaml]
+ before_action :authenticate_token!, only: [:build]
+ before_action :no_cache, only: [:badge]
+ protect_from_forgery except: :build
+
+ layout 'ci/project', except: [:index]
+
+ def show
+ @ref = params[:ref]
+
+ @commits = @project.commits.reverse_order
+ if @ref
+ # unscope is required, because of default_scope defined in Ci::Build
+ builds = @project.builds.unscope(:select, :order).where(ref: @ref).select(:commit_id).distinct
+ @commits = @commits.where(id: builds)
+ end
+ @commits = @commits.page(params[:page]).per(20)
+ 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 namespace_project_runners_path(project.gl_project.namespace, project.gl_project)
+ 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
+ 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..97f01d40af5
--- /dev/null
+++ b/app/controllers/ci/runner_projects_controller.rb
@@ -0,0 +1,36 @@
+module Ci
+ class RunnerProjectsController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_manage_project!
+
+ layout 'ci/project'
+
+ def create
+ @runner = Ci::Runner.find(params[:runner_project][:runner_id])
+
+ return head(403) unless current_user.ci_authorized_runners.include?(@runner)
+
+ path = runners_path(@project.gl_project)
+
+ if @runner.assign_to(project, current_user)
+ redirect_to path
+ else
+ redirect_to path, alert: 'Failed adding runner to project'
+ end
+ end
+
+ def destroy
+ runner_project = project.runner_projects.find(params[:id])
+ runner_project.destroy
+
+ redirect_to runners_path(@project.gl_project)
+ end
+
+ private
+
+ def project
+ @project ||= Ci::Project.find(params[:project_id])
+ 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..52c96a34ce8
--- /dev/null
+++ b/app/controllers/ci/services_controller.rb
@@ -0,0 +1,59 @@
+module Ci
+ class ServicesController < Ci::ApplicationController
+ before_action :authenticate_user!
+ before_action :project
+ before_action :authorize_access_project!
+ before_action :authorize_manage_project!
+ before_action :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)
+ 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/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index da96171e885..58e9049f158 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -1,10 +1,26 @@
class Dashboard::ProjectsController < Dashboard::ApplicationController
before_action :event_filter
+ def index
+ @projects = current_user.authorized_projects.sorted_by_activity.non_archived
+ @projects = @projects.includes(:namespace)
+ @last_push = current_user.recent_push
+
+ respond_to do |format|
+ format.html
+ format.atom do
+ event_filter
+ load_events
+ render layout: false
+ end
+ end
+ end
+
def starred
@projects = current_user.starred_projects
@projects = @projects.includes(:namespace, :forked_from_project, :tags)
@projects = @projects.sort(@sort = params[:sort])
+ @last_push = current_user.recent_push
@groups = []
respond_to do |format|
diff --git a/app/controllers/dashboard/snippets_controller.rb b/app/controllers/dashboard/snippets_controller.rb
new file mode 100644
index 00000000000..f4354c6d8ca
--- /dev/null
+++ b/app/controllers/dashboard/snippets_controller.rb
@@ -0,0 +1,10 @@
+class Dashboard::SnippetsController < Dashboard::ApplicationController
+ def index
+ @snippets = SnippetsFinder.new.execute(current_user,
+ filter: :by_user,
+ user: current_user,
+ scope: params[:scope]
+ )
+ @snippets = @snippets.page(params[:page]).per(PER_PAGE)
+ end
+end
diff --git a/app/controllers/dashboard_controller.rb b/app/controllers/dashboard_controller.rb
index d745131694b..4ebb3d7276e 100644
--- a/app/controllers/dashboard_controller.rb
+++ b/app/controllers/dashboard_controller.rb
@@ -1,23 +1,8 @@
class DashboardController < Dashboard::ApplicationController
- before_action :load_projects
before_action :event_filter, only: :activity
respond_to :html
- def show
- @projects = @projects.includes(:namespace)
- @last_push = current_user.recent_push
-
- respond_to do |format|
- format.html
- format.atom do
- event_filter
- load_events
- render layout: false
- end
- end
- end
-
def merge_requests
@merge_requests = get_merge_requests_collection
@merge_requests = @merge_requests.page(params[:page]).per(PER_PAGE)
@@ -50,12 +35,15 @@ class DashboardController < Dashboard::ApplicationController
protected
- def load_projects
- @projects = current_user.authorized_projects.sorted_by_activity.non_archived
- end
-
def load_events
- @events = Event.in_projects(current_user.authorized_projects.pluck(:id))
+ project_ids =
+ if params[:filter] == "starred"
+ current_user.starred_projects
+ else
+ current_user.authorized_projects
+ end.pluck(:id)
+
+ @events = Event.in_projects(project_ids)
@events = @event_filter.apply_filter(@events).with_associations
@events = @events.limit(20).offset(params[:offset] || 0)
end
diff --git a/app/controllers/explore/application_controller.rb b/app/controllers/explore/application_controller.rb
index 4b275033d26..461fc059a3c 100644
--- a/app/controllers/explore/application_controller.rb
+++ b/app/controllers/explore/application_controller.rb
@@ -1,3 +1,5 @@
class Explore::ApplicationController < ApplicationController
+ skip_before_action :authenticate_user!, :reject_blocked
+
layout 'explore'
end
diff --git a/app/controllers/explore/groups_controller.rb b/app/controllers/explore/groups_controller.rb
index 55cda0cff17..9575a87ee41 100644
--- a/app/controllers/explore/groups_controller.rb
+++ b/app/controllers/explore/groups_controller.rb
@@ -1,7 +1,4 @@
class Explore::GroupsController < Explore::ApplicationController
- skip_before_action :authenticate_user!,
- :reject_blocked, :set_current_user_for_observers
-
def index
@groups = GroupsFinder.new.execute(current_user)
@groups = @groups.search(params[:search]) if params[:search].present?
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 6c733c1ae4d..a5aeaed66c5 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -1,7 +1,4 @@
class Explore::ProjectsController < Explore::ApplicationController
- skip_before_action :authenticate_user!,
- :reject_blocked
-
def index
@projects = ProjectsFinder.new.execute(current_user)
@tags = @projects.tags_on(:tags)
diff --git a/app/controllers/explore/snippets_controller.rb b/app/controllers/explore/snippets_controller.rb
new file mode 100644
index 00000000000..b70ac51d06e
--- /dev/null
+++ b/app/controllers/explore/snippets_controller.rb
@@ -0,0 +1,6 @@
+class Explore::SnippetsController < Explore::ApplicationController
+ def index
+ @snippets = SnippetsFinder.new.execute(current_user, filter: :all)
+ @snippets = @snippets.page(params[:page]).per(PER_PAGE)
+ end
+end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 279c6ef0f4d..524218290c6 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -4,7 +4,7 @@ class GroupsController < Groups::ApplicationController
before_action :group, except: [:new, :create]
# Authorize
- before_action :authorize_read_group!, except: [:new, :create]
+ before_action :authorize_read_group!, except: [:show, :new, :create]
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
before_action :authorize_create_group!, only: [:new, :create]
@@ -14,6 +14,10 @@ class GroupsController < Groups::ApplicationController
layout :determine_layout
+ def index
+ redirect_to(current_user ? dashboard_groups_path : explore_groups_path)
+ end
+
def new
@group = Group.new
end
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index 71831c5380d..55050615473 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -1,7 +1,14 @@
class HelpController < ApplicationController
+ skip_before_action :authenticate_user!, :reject_blocked
+
layout 'help'
def index
+ @help_index = File.read(Rails.root.join('doc', 'README.md'))
+
+ # Prefix Markdown links with `help/` unless they already have been
+ # See http://rubular.com/r/nwwhzH6Z8X
+ @help_index.gsub!(/(\]\()(?!help\/)([^\)\(]+)(\))/, '\1help/\2\3')
end
def show
diff --git a/app/controllers/import/fogbugz_controller.rb b/app/controllers/import/fogbugz_controller.rb
new file mode 100644
index 00000000000..849646cd665
--- /dev/null
+++ b/app/controllers/import/fogbugz_controller.rb
@@ -0,0 +1,104 @@
+class Import::FogbugzController < Import::BaseController
+ before_action :verify_fogbugz_import_enabled
+ before_action :user_map, only: [:new_user_map, :create_user_map]
+
+ rescue_from Fogbugz::AuthenticationException, with: :fogbugz_unauthorized
+
+ def new
+
+ end
+
+ def callback
+ begin
+ res = Gitlab::FogbugzImport::Client.new(import_params.symbolize_keys)
+ rescue
+ # If the URI is invalid various errors can occur
+ return redirect_to new_import_fogbugz_path, alert: 'Could not connect to FogBugz, check your URL'
+ end
+ session[:fogbugz_token] = res.get_token
+ session[:fogbugz_uri] = params[:uri]
+
+ redirect_to new_user_map_import_fogbugz_path
+ end
+
+ def new_user_map
+
+ end
+
+ def create_user_map
+ user_map = params[:users]
+
+ unless user_map.is_a?(Hash) && user_map.all? { |k, v| !v[:name].blank? }
+ flash.now[:alert] = 'All users must have a name.'
+
+ render 'new_user_map' and return
+ end
+
+ session[:fogbugz_user_map] = user_map
+
+ flash[:notice] = 'The user map has been saved. Continue by selecting the projects you want to import.'
+
+ redirect_to status_import_fogbugz_path
+ end
+
+ def status
+ unless client.valid?
+ return redirect_to new_import_fogbugz_path
+ end
+
+ @repos = client.repos
+
+ @already_added_projects = current_user.created_projects.where(import_type: 'fogbugz')
+ already_added_projects_names = @already_added_projects.pluck(:import_source)
+
+ @repos.reject! { |repo| already_added_projects_names.include? repo.name }
+ end
+
+ def jobs
+ jobs = current_user.created_projects.where(import_type: 'fogbugz').to_json(only: [:id, :import_status])
+ render json: jobs
+ end
+
+ def create
+ @repo_id = params[:repo_id]
+ repo = client.repo(@repo_id)
+ fb_session = { uri: session[:fogbugz_uri], token: session[:fogbugz_token] }
+ @target_namespace = current_user.namespace
+ @project_name = repo.name
+
+ namespace = @target_namespace
+
+ umap = session[:fogbugz_user_map] || client.user_map
+
+ @project = Gitlab::FogbugzImport::ProjectCreator.new(repo, fb_session, namespace, current_user, umap).execute
+ end
+
+ private
+
+ def client
+ @client ||= Gitlab::FogbugzImport::Client.new(token: session[:fogbugz_token], uri: session[:fogbugz_uri])
+ end
+
+ def user_map
+ @user_map ||= begin
+ user_map = client.user_map
+
+ stored_user_map = session[:fogbugz_user_map]
+ user_map.update(stored_user_map) if stored_user_map
+
+ user_map
+ end
+ end
+
+ def fogbugz_unauthorized(exception)
+ redirect_to new_import_fogbugz_path, alert: exception.message
+ end
+
+ def import_params
+ params.permit(:uri, :email, :password)
+ end
+
+ def verify_fogbugz_import_enabled
+ not_found! unless fogbugz_import_enabled?
+ end
+end
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index eb3c8233530..8ef10a17f55 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -24,7 +24,7 @@ class InvitesController < ApplicationController
path =
if current_user
- dashboard_path
+ dashboard_projects_path
else
new_user_session_path
end
@@ -73,7 +73,7 @@ class InvitesController < ApplicationController
path = group_path(group)
else
label = "who knows what"
- path = dashboard_path
+ path = dashboard_projects_path
end
[label, path]
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
index 83eec1bf4a2..282012c60a1 100644
--- a/app/controllers/namespaces_controller.rb
+++ b/app/controllers/namespaces_controller.rb
@@ -14,7 +14,7 @@ class NamespacesController < ApplicationController
if user
redirect_to user_path(user)
- elsif group && can?(current_user, :read_group, group)
+ elsif group
redirect_to group_path(group)
elsif current_user.nil?
authenticate_user!
diff --git a/app/controllers/oauth/applications_controller.rb b/app/controllers/oauth/applications_controller.rb
index fc31118124b..dc22101cd5e 100644
--- a/app/controllers/oauth/applications_controller.rb
+++ b/app/controllers/oauth/applications_controller.rb
@@ -1,7 +1,7 @@
class Oauth::ApplicationsController < Doorkeeper::ApplicationsController
include Gitlab::CurrentSettings
include PageLayoutHelper
-
+
before_action :verify_user_oauth_applications_enabled
before_action :authenticate_user!
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 523264b8ea9..f809fa7500a 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -71,7 +71,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to omniauth_error_path(oauth['provider'], error: error_message) and return
end
end
- rescue Gitlab::OAuth::SignupDisabledError => e
+ rescue Gitlab::OAuth::SignupDisabledError
label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
@@ -80,7 +80,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
flash[:notice] = message
-
+
redirect_to new_user_session_path
end
diff --git a/app/controllers/passwords_controller.rb b/app/controllers/passwords_controller.rb
index 8450ba31021..2025158d065 100644
--- a/app/controllers/passwords_controller.rb
+++ b/app/controllers/passwords_controller.rb
@@ -1,41 +1,7 @@
class PasswordsController < Devise::PasswordsController
-
- def create
- email = resource_params[:email]
- resource_found = resource_class.find_by_email(email)
- if resource_found && resource_found.ldap_user?
- flash[:alert] = "Cannot reset password for LDAP user."
- respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name)) and return
- end
-
- self.resource = resource_class.send_reset_password_instructions(resource_params)
- if successfully_sent?(resource)
- respond_with({}, location: after_sending_reset_password_instructions_path_for(resource_name))
- else
- respond_with(resource)
- end
- end
-
- # After a user resets their password, prompt for 2FA code if enabled instead
- # of signing in automatically
- #
- # See http://git.io/vURrI
- def update
- super do |resource|
- # TODO (rspeicher): In Devise master (> 3.4.1), we can set
- # `Devise.sign_in_after_reset_password = false` and avoid this mess.
- if resource.errors.empty? && resource.try(:two_factor_enabled?)
- resource.unlock_access! if unlockable?(resource)
-
- # Since we are not signing this user in, we use the :updated_not_active
- # message which only contains "Your password was changed successfully."
- set_flash_message(:notice, :updated_not_active) if is_flashing_format?
-
- # Redirect to sign in so they can enter 2FA code
- respond_with(resource, location: new_session_path(resource)) and return
- end
- end
- end
+ before_action :resource_from_email, only: [:create]
+ before_action :prevent_ldap_reset, only: [:create]
+ before_action :throttle_reset, only: [:create]
def edit
super
@@ -56,4 +22,25 @@ class PasswordsController < Devise::PasswordsController
end
end
end
+
+ protected
+
+ def resource_from_email
+ email = resource_params[:email]
+ self.resource = resource_class.find_by_email(email)
+ end
+
+ def prevent_ldap_reset
+ return unless resource && resource.ldap_user?
+
+ redirect_to after_sending_reset_password_instructions_path_for(resource_name),
+ alert: "Cannot reset password for LDAP user."
+ end
+
+ def throttle_reset
+ return unless resource && resource.recently_sent_password_reset?
+
+ redirect_to new_password_path(resource_name),
+ alert: I18n.t('devise.passwords.recently_reset')
+ end
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index f83b4abd1e2..a9a06ecc808 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -31,6 +31,7 @@ class Profiles::PreferencesController < Profiles::ApplicationController
def preferences_params
params.require(:user).permit(
:color_scheme_id,
+ :layout,
:dashboard,
:project_view,
:theme_id
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index f9af0871cf1..e6b99be37fb 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -9,7 +9,7 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
def create
- if current_user.valid_otp?(params[:pin_code])
+ if current_user.validate_and_consume_otp!(params[:pin_code])
current_user.two_factor_enabled = true
@codes = current_user.generate_otp_backup_codes!
current_user.save!
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index ee88d49b400..519d6d6127e 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -25,4 +25,14 @@ class Projects::ApplicationController < ApplicationController
)
end
end
+
+ private
+
+ def ci_enabled
+ return render_404 unless @project.gitlab_ci?
+ end
+
+ def ci_project
+ @ci_project ||= @project.ensure_gitlab_ci_project
+ end
end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 100d3d3b317..8776721d243 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -26,10 +26,16 @@ class Projects::BlobController < Projects::ApplicationController
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
- redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path))
+ respond_to do |format|
+ format.html { redirect_to namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) }
+ format.json { render json: { message: "success", filePath: namespace_project_blob_path(@project.namespace, @project, File.join(@target_branch, @file_path)) } }
+ end
else
flash[:alert] = result[:message]
- render :new
+ respond_to do |format|
+ format.html { render :new }
+ format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
+ end
end
end
@@ -45,10 +51,16 @@ class Projects::BlobController < Projects::ApplicationController
if result[:status] == :success
flash[:notice] = "Your changes have been successfully committed"
- redirect_to after_edit_path
+ respond_to do |format|
+ format.html { redirect_to after_edit_path }
+ format.json { render json: { message: "success", filePath: after_edit_path } }
+ end
else
flash[:alert] = result[:message]
- render :edit
+ respond_to do |format|
+ format.html { render :edit }
+ format.json { render json: { message: "failed", filePath: namespace_project_new_blob_path(@project.namespace, @project, @id) } }
+ end
end
end
@@ -146,11 +158,19 @@ class Projects::BlobController < Projects::ApplicationController
@file_path =
if action_name.to_s == 'create'
+ if params[:file].present?
+ params[:file_name] = params[:file].original_filename
+ end
File.join(@path, File.basename(params[:file_name]))
else
@path
end
+ if params[:file].present?
+ params[:content] = Base64.encode64(params[:file].read)
+ params[:encoding] = 'base64'
+ end
+
@commit_params = {
file_path: @file_path,
current_branch: @current_branch,
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
new file mode 100644
index 00000000000..76c7f31f61b
--- /dev/null
+++ b/app/controllers/projects/builds_controller.rb
@@ -0,0 +1,25 @@
+class Projects::BuildsController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :build
+
+ layout "project"
+
+ def show
+ @builds = @ci_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
+
+ private
+
+ def build
+ @build ||= ci_project.builds.unscoped.find_by!(id: params[:id])
+ end
+end
diff --git a/app/controllers/projects/ci_settings_controller.rb b/app/controllers/projects/ci_settings_controller.rb
new file mode 100644
index 00000000000..a263242a850
--- /dev/null
+++ b/app/controllers/projects/ci_settings_controller.rb
@@ -0,0 +1,36 @@
+class Projects::CiSettingsController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout "project_settings"
+
+ def edit
+ end
+
+ def update
+ if ci_project.update_attributes(project_params)
+ Ci::EventService.new.change_project_settings(current_user, ci_project)
+
+ redirect_to edit_namespace_project_ci_settings_path(project.namespace, project), notice: 'Project was successfully updated.'
+ else
+ render action: "edit"
+ end
+ end
+
+ def destroy
+ ci_project.destroy
+ Ci::EventService.new.remove_project(current_user, ci_project)
+ project.gitlab_ci_service.update_attributes(active: false)
+
+ redirect_to project_path(project), notice: "CI was disabled for this project"
+ end
+
+ protected
+
+ 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
diff --git a/app/controllers/projects/ci_web_hooks_controller.rb b/app/controllers/projects/ci_web_hooks_controller.rb
new file mode 100644
index 00000000000..7f40ddcb3f3
--- /dev/null
+++ b/app/controllers/projects/ci_web_hooks_controller.rb
@@ -0,0 +1,45 @@
+class Projects::CiWebHooksController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout "project_settings"
+
+ def index
+ @web_hooks = @ci_project.web_hooks
+ @web_hook = Ci::WebHook.new
+ end
+
+ def create
+ @web_hook = @ci_project.web_hooks.new(web_hook_params)
+ @web_hook.save
+
+ if @web_hook.valid?
+ redirect_to namespace_project_ci_web_hooks_path(@project.namespace, @project)
+ else
+ @web_hooks = @ci_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 namespace_project_ci_web_hooks_path(@project.namespace, @project)
+ end
+
+ private
+
+ def hook
+ @web_hook ||= @ci_project.web_hooks.find(params[:id])
+ end
+
+ def web_hook_params
+ params.require(:web_hook).permit(:url)
+ end
+end
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 78d42d695b6..1938c63c10c 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -22,6 +22,8 @@ class Projects::CommitController < Projects::ApplicationController
commit_id: @commit.id
}
+ @ci_commit = project.ci_commit(commit.sha)
+
respond_to do |format|
format.html
format.diff { render text: @commit.to_diff }
@@ -29,6 +31,13 @@ class Projects::CommitController < Projects::ApplicationController
end
end
+ def ci
+ @ci_commit = @project.ci_commit(@commit.sha)
+ @builds = @ci_commit.builds if @ci_commit
+ @notes_count = @commit.notes.count
+ @ci_project = @project.gitlab_ci_project
+ end
+
def branches
@branches = @project.repository.branch_names_contains(commit.id)
@tags = @project.repository.tag_names_contains(commit.id)
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index d9b3adae95b..d15004f93a6 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -16,10 +16,12 @@ class Projects::CompareController < Projects::ApplicationController
compare_result = CompareService.new.
execute(@project, head_ref, @project, base_ref)
- @commits = compare_result.commits
- @diffs = compare_result.diffs
- @commit = @commits.last
- @line_notes = []
+ if compare_result
+ @commits = compare_result.commits
+ @diffs = compare_result.diffs
+ @commit = @commits.last
+ @line_notes = []
+ end
end
def create
diff --git a/app/controllers/projects/forks_controller.rb b/app/controllers/projects/forks_controller.rb
index 9e72597ea87..8a785076bb7 100644
--- a/app/controllers/projects/forks_controller.rb
+++ b/app/controllers/projects/forks_controller.rb
@@ -13,10 +13,14 @@ class Projects::ForksController < Projects::ApplicationController
@forked_project = ::Projects::ForkService.new(project, current_user, namespace: namespace).execute
if @forked_project.saved? && @forked_project.forked?
- redirect_to(
- namespace_project_path(@forked_project.namespace, @forked_project),
- notice: 'Project was successfully forked.'
- )
+ if @forked_project.import_in_progress?
+ redirect_to namespace_project_import_path(@forked_project.namespace, @forked_project)
+ else
+ redirect_to(
+ namespace_project_path(@forked_project.namespace, @forked_project),
+ notice: 'Project was successfully forked.'
+ )
+ end
else
render :error
end
diff --git a/app/controllers/projects/graphs_controller.rb b/app/controllers/projects/graphs_controller.rb
index 0b6f7f5c91e..418b92040bc 100644
--- a/app/controllers/projects/graphs_controller.rb
+++ b/app/controllers/projects/graphs_controller.rb
@@ -5,6 +5,7 @@ class Projects::GraphsController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :assign_ref_vars
before_action :authorize_download_code!
+ before_action :ci_enabled, only: :ci
def show
respond_to do |format|
@@ -23,6 +24,16 @@ class Projects::GraphsController < Projects::ApplicationController
@commits_per_month = @commits_graph.commits_per_month
end
+ def ci
+ ci_project = @project.gitlab_ci_project
+
+ @charts = {}
+ @charts[:week] = Ci::Charts::WeekChart.new(ci_project)
+ @charts[:month] = Ci::Charts::MonthChart.new(ci_project)
+ @charts[:year] = Ci::Charts::YearChart.new(ci_project)
+ @charts[:build_times] = Ci::Charts::BuildTime.new(ci_project)
+ end
+
private
def fetch_graph
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index f3054881daf..7570934e727 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -7,6 +7,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :closes_issues, only: [:edit, :update, :show, :diffs, :commits]
before_action :validates_merge_request, only: [:show, :diffs, :commits]
before_action :define_show_vars, only: [:show, :diffs, :commits]
+ before_action :ensure_ref_fetched, only: [:show, :commits, :diffs]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@@ -149,6 +150,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return access_denied! unless @merge_request.can_be_merged_by?(current_user)
if @merge_request.mergeable?
+ @merge_request.update(merge_error: nil)
MergeWorker.perform_async(@merge_request.id, current_user.id, params)
@status = true
else
@@ -257,8 +259,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@commits = @merge_request.commits
@merge_request_diff = @merge_request.merge_request_diff
- @source_branch = @merge_request.source_project.repository.find_branch(@merge_request.source_branch).try(:name)
-
+
if @merge_request.locked_long_ago?
@merge_request.unlock_mr
@merge_request.close
@@ -277,4 +278,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
:state_event, :description, :task_num, label_ids: []
)
end
+
+ # Make sure merge requests created before 8.0
+ # have head file in refs/merge-requests/
+ def ensure_ref_fetched
+ @merge_request.ensure_ref_fetched
+ end
end
diff --git a/app/controllers/projects/milestones_controller.rb b/app/controllers/projects/milestones_controller.rb
index 9efe9704d1e..86f4a02a6e9 100644
--- a/app/controllers/projects/milestones_controller.rb
+++ b/app/controllers/projects/milestones_controller.rb
@@ -66,12 +66,7 @@ class Projects::MilestonesController < Projects::ApplicationController
def destroy
return access_denied! unless can?(current_user, :admin_milestone, @project)
- update_params = { milestone: nil }
- @milestone.issues.each do |issue|
- Issues::UpdateService.new(@project, current_user, update_params).execute(issue)
- end
-
- @milestone.destroy
+ Milestones::DestroyService.new(project, current_user).execute(milestone)
respond_to do |format|
format.html { redirect_to namespace_project_milestones_path }
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index b82b6f45d59..cf73bc01c8f 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -78,7 +78,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project.project_members.find_by(user_id: current_user).destroy
respond_to do |format|
- format.html { redirect_to dashboard_path }
+ format.html { redirect_to dashboard_projects_path }
format.js { render nothing: true }
end
end
diff --git a/app/controllers/projects/raw_controller.rb b/app/controllers/projects/raw_controller.rb
index 647c1454078..5f6fbce795e 100644
--- a/app/controllers/projects/raw_controller.rb
+++ b/app/controllers/projects/raw_controller.rb
@@ -17,8 +17,7 @@ class Projects::RawController < Projects::ApplicationController
send_data(
@blob.data,
type: type,
- disposition: 'inline',
- filename: @blob.name
+ disposition: 'inline'
)
else
not_found!
@@ -30,6 +29,8 @@ class Projects::RawController < Projects::ApplicationController
def get_blob_type
if @blob.text?
'text/plain; charset=utf-8'
+ elsif @blob.image?
+ @blob.content_type
else
'application/octet-stream'
end
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
new file mode 100644
index 00000000000..6cb6e3ef6d4
--- /dev/null
+++ b/app/controllers/projects/runners_controller.rb
@@ -0,0 +1,65 @@
+class Projects::RunnersController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :set_runner, only: [:edit, :update, :destroy, :pause, :resume, :show]
+ before_action :authorize_admin_project!
+
+ layout 'project_settings'
+
+ def index
+ @runners = @ci_project.runners.order('id DESC')
+ @specific_runners =
+ Ci::Runner.specific.includes(:runner_projects).
+ where(Ci::RunnerProject.table_name => { project_id: current_user.authorized_projects } ).
+ where.not(id: @runners).order("#{Ci::Runner.table_name}.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 runner_path(@runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def destroy
+ if @runner.only_for?(@ci_project)
+ @runner.destroy
+ end
+
+ redirect_to runners_path(@project)
+ end
+
+ def resume
+ if @runner.update_attributes(active: true)
+ redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def pause
+ if @runner.update_attributes(active: false)
+ redirect_to runner_path(@runner), notice: 'Runner was successfully updated.'
+ else
+ redirect_to runner_path(@runner), alert: 'Runner was not updated.'
+ end
+ end
+
+ def show
+ end
+
+ protected
+
+ def set_runner
+ @runner ||= @ci_project.runners.find(params[:id])
+ end
+
+ def runner_params
+ params.require(:runner).permit(:description, :tag_list, :contacted_at, :active)
+ end
+end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index b0cf5866d41..3047ee8a1ff 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -2,7 +2,7 @@ class Projects::ServicesController < Projects::ApplicationController
ALLOWED_PARAMS = [:title, :token, :type, :active, :api_key, :api_version, :subdomain,
:room, :recipients, :project_url, :webhook,
:user_key, :device, :priority, :sound, :bamboo_url, :username, :password,
- :build_key, :server, :teamcity_url, :build_type,
+ :build_key, :server, :teamcity_url, :drone_url, :build_type,
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
@@ -58,6 +58,8 @@ class Projects::ServicesController < Projects::ApplicationController
end
def service_params
- params.require(:service).permit(ALLOWED_PARAMS)
+ service_params = params.require(:service).permit(ALLOWED_PARAMS)
+ service_params.delete("password") if service_params["password"].blank?
+ service_params
end
end
diff --git a/app/controllers/projects/triggers_controller.rb b/app/controllers/projects/triggers_controller.rb
new file mode 100644
index 00000000000..782ebd01b05
--- /dev/null
+++ b/app/controllers/projects/triggers_controller.rb
@@ -0,0 +1,35 @@
+class Projects::TriggersController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout 'project_settings'
+
+ def index
+ @triggers = @ci_project.triggers
+ @trigger = Ci::Trigger.new
+ end
+
+ def create
+ @trigger = @ci_project.triggers.new
+ @trigger.save
+
+ if @trigger.valid?
+ redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ else
+ @triggers = @ci_project.triggers.select(&:persisted?)
+ render :index
+ end
+ end
+
+ def destroy
+ trigger.destroy
+
+ redirect_to namespace_project_triggers_path(@project.namespace, @project)
+ end
+
+ private
+
+ def trigger
+ @trigger ||= @ci_project.triggers.find(params[:id])
+ end
+end
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
new file mode 100644
index 00000000000..d6561a45a70
--- /dev/null
+++ b/app/controllers/projects/variables_controller.rb
@@ -0,0 +1,25 @@
+class Projects::VariablesController < Projects::ApplicationController
+ before_action :ci_project
+ before_action :authorize_admin_project!
+
+ layout 'project_settings'
+
+ def show
+ end
+
+ def update
+ if ci_project.update_attributes(project_params)
+ Ci::EventService.new.change_project_settings(current_user, ci_project)
+
+ redirect_to namespace_project_variables_path(project.namespace, project), notice: 'Variables were successfully updated.'
+ else
+ render action: 'show'
+ end
+ end
+
+ private
+
+ def project_params
+ params.require(:project).permit({ variables_attributes: [:id, :key, :value, :_destroy] })
+ end
+end
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 50512cb6dc3..88fccfed509 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -5,7 +5,6 @@ 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
def pages
@wiki_pages = Kaminari.paginate_array(@project_wiki.pages).page(params[:page]).per(PER_PAGE)
@@ -99,7 +98,7 @@ class Projects::WikisController < Projects::ApplicationController
# Call #wiki to make sure the Wiki Repo is initialized
@project_wiki.wiki
- rescue ProjectWiki::CouldNotCreateWikiError => ex
+ rescue ProjectWiki::CouldNotCreateWikiError
flash[:notice] = "Could not create Wiki Repository at this time. Please try again later."
redirect_to project_path(@project)
return false
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index dafc11d0707..213c2a7173b 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -10,6 +10,10 @@ class ProjectsController < ApplicationController
layout :determine_layout
+ def index
+ redirect_to(current_user ? root_path : explore_root_path)
+ end
+
def new
@project = Project.new
end
@@ -82,6 +86,10 @@ class ProjectsController < ApplicationController
if @project.empty_repo?
render 'projects/empty'
else
+ if current_user
+ @membership = @project.project_member_by_id(current_user.id)
+ end
+
render :show
end
else
@@ -105,7 +113,7 @@ class ProjectsController < ApplicationController
if request.referer.include?('/admin')
redirect_to admin_namespaces_projects_path
else
- redirect_to dashboard_path
+ redirect_to dashboard_projects_path
end
rescue Projects::DestroyService::DestroyError => ex
redirect_to edit_project_path(@project), alert: ex.message
diff --git a/app/controllers/root_controller.rb b/app/controllers/root_controller.rb
index fdfe00dc135..ad04c646e1b 100644
--- a/app/controllers/root_controller.rb
+++ b/app/controllers/root_controller.rb
@@ -6,10 +6,10 @@
#
# For users who haven't customized the setting, we simply delegate to
# `DashboardController#show`, which is the default.
-class RootController < DashboardController
- before_action :redirect_to_custom_dashboard, only: [:show]
+class RootController < Dashboard::ProjectsController
+ before_action :redirect_to_custom_dashboard, only: [:index]
- def show
+ def index
super
end
@@ -20,7 +20,12 @@ class RootController < DashboardController
case current_user.dashboard
when 'stars'
+ flash.keep
redirect_to starred_dashboard_projects_path
+ when 'project_activity'
+ redirect_to activity_dashboard_path
+ when 'starred_project_activity'
+ redirect_to activity_dashboard_path(filter: 'starred')
else
return
end
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 8389f07a3bd..1b60d3e27d0 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -8,6 +8,8 @@ class SessionsController < Devise::SessionsController
def new
if Gitlab.config.ldap.enabled
@ldap_servers = Gitlab::LDAP::Config.servers
+ else
+ @ldap_servers = []
end
super
@@ -97,7 +99,7 @@ class SessionsController < Devise::SessionsController
end
def valid_otp_attempt?(user)
- user.valid_otp?(user_params[:otp_attempt]) ||
+ user.validate_and_consume_otp!(user_params[:otp_attempt]) ||
user.invalidate_otp_backup_code!(user_params[:otp_attempt])
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 8e7e45c781f..9f9f9a92f11 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -24,13 +24,9 @@ class SnippetsController < ApplicationController
scope: params[:scope] }).
page(params[:page]).per(PER_PAGE)
- if @user == current_user
- render 'current_user_index'
- else
- render 'user_index'
- end
+ render 'index'
else
- @snippets = SnippetsFinder.new.execute(current_user, filter: :all).page(params[:page]).per(PER_PAGE)
+ redirect_to(current_user ? dashboard_snippets_path : explore_snippets_path)
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index ab89aa2c53a..6aa16673d63 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -39,7 +39,7 @@ class IssuableFinder
items = by_assignee(items)
items = by_author(items)
items = by_label(items)
- items = sort(items)
+ sort(items)
end
def group
diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb
index f3f4d461efa..9ea342cb26d 100644
--- a/app/finders/trending_projects_finder.rb
+++ b/app/finders/trending_projects_finder.rb
@@ -2,21 +2,12 @@ class TrendingProjectsFinder
def execute(current_user, start_date = nil)
start_date ||= Date.today - 1.month
+ projects = projects_for(current_user)
+
# Determine trending projects based on comments count
# for period of time - ex. month
- trending_project_ids = Note.
- select("notes.project_id, count(notes.project_id) as pcount").
- where('notes.created_at > ?', start_date).
- group("project_id").
- reorder("pcount DESC").
- map(&:project_id)
-
- sql_order_ids = trending_project_ids.reverse.
- map { |project_id| "id = #{project_id}" }.join(", ")
-
- # Get list of projects that user allowed to see
- projects = projects_for(current_user)
- projects.where(id: trending_project_ids).reorder(sql_order_ids)
+ projects.joins(:notes).where('notes.created_at > ?', start_date).
+ group("projects.id").reorder("count(notes.id) DESC")
end
private
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index a803b66c502..cab2278adb7 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -13,7 +13,9 @@ module ApplicationHelper
# current_controller?(:commits) # => false
# current_controller?(:commits, :tree) # => true
def current_controller?(*args)
- args.any? { |v| v.to_s.downcase == controller.controller_name }
+ args.any? do |v|
+ v.to_s.downcase == controller.controller_name || v.to_s.downcase == controller.controller_path
+ end
end
# Check if a particular action is the current one
@@ -33,7 +35,7 @@ module ApplicationHelper
def project_icon(project_id, options = {})
project =
if project_id.is_a?(Project)
- project = project_id
+ project_id
else
Project.find_with_namespace(project_id)
end
@@ -82,7 +84,7 @@ module ApplicationHelper
end
def default_avatar
- image_path('no_avatar.png')
+ 'no_avatar.png'
end
def last_commit(project)
@@ -201,7 +203,7 @@ module ApplicationHelper
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 }
+ data: { toggle: 'tooltip', placement: placement, container: 'body' }
element += javascript_tag "$('.js-timeago').timeago()" unless skip_js
@@ -312,4 +314,8 @@ module ApplicationHelper
html.html_safe
end
+
+ def truncate_first_line(message, length = 50)
+ truncate(message.each_line.first.chomp, length: length) if message
+ end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index 0e7a37b4cc6..cd99a232403 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -1,6 +1,6 @@
module AuthHelper
PROVIDERS_WITH_ICONS = %w(twitter github gitlab bitbucket google_oauth2).freeze
- FORM_BASED_PROVIDERS = [/\Aldap/, 'kerberos'].freeze
+ FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
Gitlab.config.ldap.enabled
@@ -26,6 +26,10 @@ module AuthHelper
auth_providers.select { |provider| form_based_provider?(provider) }
end
+ def crowd_enabled?
+ auth_providers.include? :crowd
+ end
+
def button_based_providers
auth_providers.reject { |provider| form_based_provider?(provider) }
end
@@ -36,7 +40,7 @@ module AuthHelper
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}")
+ image_tag("auth_buttons/#{file_name}", alt: label, title: "Sign in with #{label}")
else
label
end
diff --git a/app/helpers/builds_helper.rb b/app/helpers/builds_helper.rb
new file mode 100644
index 00000000000..1b5a2c31d74
--- /dev/null
+++ b/app/helpers/builds_helper.rb
@@ -0,0 +1,13 @@
+module BuildsHelper
+ def build_ref_link build
+ gitlab_ref_link build.project, build.ref
+ end
+
+ def build_commit_link build
+ gitlab_commit_link build.project, build.short_sha
+ end
+
+ def build_url(build)
+ namespace_project_build_path(build.gl_project, build.project, build)
+ end
+end
diff --git a/app/helpers/ci/gitlab_helper.rb b/app/helpers/ci/gitlab_helper.rb
new file mode 100644
index 00000000000..baddbc806f2
--- /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.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/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')
+ "<a href='#{ci_project_url(project, ref: ref)}'><img src='#{url}' /></a>"
+ 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_status_helper.rb b/app/helpers/ci_status_helper.rb
new file mode 100644
index 00000000000..dbd1e26fa79
--- /dev/null
+++ b/app/helpers/ci_status_helper.rb
@@ -0,0 +1,45 @@
+module CiStatusHelper
+ def ci_status_path(ci_commit)
+ project = ci_commit.gl_project
+ ci_namespace_project_commit_path(project.namespace, project, ci_commit.sha)
+ end
+
+ def ci_status_icon(ci_commit)
+ ci_icon_for_status(ci_commit.status)
+ end
+
+ def ci_status_color(ci_commit)
+ case ci_commit.status
+ when 'success'
+ 'green'
+ when 'failed'
+ 'red'
+ when 'running', 'pending'
+ 'yellow'
+ else
+ 'gray'
+ end
+ end
+
+ def ci_status_with_icon(status)
+ content_tag :span, class: "ci-status ci-#{status}" do
+ ci_icon_for_status(status) + '&nbsp;'.html_safe + status
+ end
+ end
+
+ def ci_icon_for_status(status)
+ icon_name =
+ case status
+ when 'success'
+ 'check'
+ when 'failed'
+ 'close'
+ when 'running', 'pending'
+ 'clock-o'
+ else
+ 'circle'
+ end
+
+ icon(icon_name)
+ end
+end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index d13d80be293..9df20c9fce5 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -135,7 +135,7 @@ module CommitsHelper
# 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)
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 1bd3ec5e0e0..b896fba3704 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -137,7 +137,7 @@ module DiffHelper
# 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
+ link_to url_for(params_copy), id: "inline-diff-btn", class: (params[:view] != 'parallel' ? 'btn btn-sm active' : 'btn btn-sm') do
'Inline'
end
end
@@ -148,7 +148,7 @@ module DiffHelper
# 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
+ link_to url_for(params_copy), id: "parallel-diff-btn", class: (params[:view] == 'parallel' ? 'btn active btn-sm' : 'btn btn-sm') do
'Side-by-side'
end
end
@@ -167,4 +167,23 @@ module DiffHelper
content_tag(:span, commit_id, class: 'monospace'),
].join(' ').html_safe
end
+
+ def commit_for_diff(diff)
+ if diff.deleted_file
+ @merge_request ? @merge_request.commits.last : @commit.parents.first
+ else
+ @commit
+ end
+ end
+
+ def diff_file_html_data(project, diff_commit, diff_file)
+ {
+ blob_diff_path: namespace_project_blob_diff_path(project.namespace, project,
+ tree_join(diff_commit.id, diff_file.file_path))
+ }
+ end
+
+ def editable_diff?(diff)
+ !diff.deleted_file && @merge_request && @merge_request.source_project
+ end
end
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 8428281f8f6..6f69c2a9f32 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -27,16 +27,13 @@ module EventsHelper
key = key.to_s
active = 'active' if @event_filter.active?(key)
link_opts = {
- class: 'event_filter_link',
+ class: "event-filter-link btn btn-default #{active}",
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
+ link_to request.path, link_opts do
+ content_tag(:span, ' ' + tooltip)
end
end
@@ -49,6 +46,14 @@ module EventsHelper
}
end
+ def event_preposition(event)
+ if event.push? || event.commented? || event.target
+ "at"
+ elsif event.milestone?
+ "in"
+ end
+ end
+
def event_feed_title(event)
words = []
words << event.author_name
@@ -65,8 +70,11 @@ module EventsHelper
words << "##{truncate event.note_target_iid}"
end
words << "at"
+ elsif event.milestone?
+ words << "##{event.target_iid}" if event.target_iid
+ words << "in"
elsif event.target
- words << "##{event.target_iid}:"
+ words << "##{event.target_iid}:"
words << event.target.title if event.target.respond_to?(:title)
words << "at"
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 0d175e1ea18..40161c5a641 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -21,7 +21,7 @@ module GitlabMarkdownHelper
gfm_body = Gitlab::Markdown.gfm(escaped_body, project: @project, current_user: current_user)
- fragment = Nokogiri::XML::DocumentFragment.parse(gfm_body)
+ fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
# Fragment has only one node, and it's a link generated by `gfm`.
# Replace it with our requested link.
@@ -171,7 +171,7 @@ module GitlabMarkdownHelper
# 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
+ node.inner_html = "#{node.inner_html}..." if node.next_sibling
true
else
truncated
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index d0fae255a04..4d9da6ff837 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -17,6 +17,14 @@ module GitlabRoutingHelper
namespace_project_path(project.namespace, project, *args)
end
+ def project_files_path(project, *args)
+ namespace_project_tree_path(project.namespace, project, @ref || project.repository.root_ref)
+ end
+
+ def project_commits_path(project, *args)
+ namespace_project_commits_path(project.namespace, project, @ref || project.repository.root_ref)
+ end
+
def activity_project_path(project, *args)
activity_namespace_project_path(project.namespace, project, *args)
end
@@ -25,6 +33,14 @@ module GitlabRoutingHelper
edit_namespace_project_path(project.namespace, project, *args)
end
+ def runners_path(project, *args)
+ namespace_project_runners_path(project.namespace, project, *args)
+ end
+
+ def runner_path(runner, *args)
+ namespace_project_runner_path(@project.namespace, @project, runner, *args)
+ end
+
def issue_path(entity, *args)
namespace_project_issue_path(entity.project.namespace, entity.project, entity, *args)
end
diff --git a/app/helpers/graph_helper.rb b/app/helpers/graph_helper.rb
index e1dda20de85..1e372d5631d 100644
--- a/app/helpers/graph_helper.rb
+++ b/app/helpers/graph_helper.rb
@@ -1,7 +1,10 @@
module GraphHelper
def get_refs(repo, commit)
refs = ""
- refs << commit.ref_names(repo).join(' ')
+ # Commit::ref_names already strips the refs/XXX from important refs (e.g. refs/heads/XXX)
+ # so anything leftover is internally used by GitLab
+ commit_refs = commit.ref_names(repo).reject{ |name| name.starts_with?('refs/') }
+ refs << commit_refs.join(' ')
# append note count
refs << "[#{@graph.notes[commit.id]}]" if @graph.notes[commit.id] > 0
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index b067cb54a43..1d36969cd62 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -27,7 +27,16 @@ module GroupsHelper
if group && group.avatar.present?
group.avatar.url
else
- image_path('no_group_avatar.png')
+ 'no_group_avatar.png'
+ end
+ end
+
+ def group_title(group, name = nil, url = nil)
+ full_title = link_to(simple_sanitize(group.name), group_path(group))
+ full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
+
+ content_tag :span do
+ full_title
end
end
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index f8169b4f288..81773e7afcf 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -71,4 +71,17 @@ module MergeRequestsHelper
merge_request.source_branch
end
end
+
+ def format_mr_branch_names(merge_request)
+ source_path = merge_request.source_project_path
+ target_path = merge_request.target_project_path
+ source_branch = merge_request.source_branch
+ target_branch = merge_request.target_branch
+
+ if source_path == target_path
+ [source_branch, target_branch]
+ else
+ ["#{source_path}:#{source_branch}", "#{target_path}:#{target_branch}"]
+ end
+ end
end
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 2f8e64c375f..cf11f8e5320 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -12,4 +12,49 @@ module NotificationsHelper
icon('circle-o', class: 'ns-default')
end
end
+
+ def notification_list_item(notification_level, user_membership)
+ case notification_level
+ when Notification::N_DISABLED
+ content_tag(:li, class: active_level_for(user_membership, Notification::N_DISABLED)) do
+ link_to '#', class: 'update-notification', data: { notification_level: Notification::N_DISABLED } do
+ icon('microphone-slash fw', text: 'Disabled')
+ end
+ end
+ when Notification::N_PARTICIPATING
+ content_tag(:li, class: active_level_for(user_membership, Notification::N_PARTICIPATING)) do
+ link_to '#', class: 'update-notification', data: { notification_level: Notification::N_PARTICIPATING } do
+ icon('volume-up fw', text: 'Participate')
+ end
+ end
+ when Notification::N_WATCH
+ content_tag(:li, class: active_level_for(user_membership, Notification::N_WATCH)) do
+ link_to '#', class: 'update-notification', data: { notification_level: Notification::N_WATCH } do
+ icon('eye fw', text: 'Watch')
+ end
+ end
+ when Notification::N_MENTION
+ content_tag(:li, class: active_level_for(user_membership, Notification::N_MENTION)) do
+ link_to '#', class: 'update-notification', data: { notification_level: Notification::N_MENTION } do
+ icon('at fw', text: 'On mention')
+ end
+ end
+ when Notification::N_GLOBAL
+ content_tag(:li, class: active_level_for(user_membership, Notification::N_GLOBAL)) do
+ link_to '#', class: 'update-notification', data: { notification_level: Notification::N_GLOBAL } do
+ icon('globe fw', text: 'Global')
+ end
+ end
+ else
+ # do nothing
+ end
+ end
+
+ def notification_label(user_membership)
+ Notification.new(user_membership).to_s
+ end
+
+ def active_level_for(user_membership, level)
+ 'active' if user_membership.notification_level == level
+ end
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 8473d6d75d0..775cf5a3dd4 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -26,9 +26,31 @@ module PageLayoutHelper
def fluid_layout(enabled = false)
if @fluid_layout.nil?
- @fluid_layout = enabled
+ @fluid_layout = (current_user && current_user.layout == "fluid") || enabled
else
@fluid_layout
end
end
+
+ def blank_container(enabled = false)
+ if @blank_container.nil?
+ @blank_container = enabled
+ else
+ @blank_container
+ end
+ end
+
+ def container_class
+ css_class = "container-fluid"
+
+ unless fluid_layout
+ css_class += " container-limited"
+ end
+
+ if blank_container
+ css_class += " container-blank"
+ end
+
+ css_class
+ end
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index 7f1b6a69926..4710171ebaa 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -1,9 +1,18 @@
# Helper methods for per-User preferences
module PreferencesHelper
+ def layout_choices
+ [
+ ['Fixed', :fixed],
+ ['Fluid', :fluid]
+ ]
+ end
+
# Maps `dashboard` values to more user-friendly option text
DASHBOARD_CHOICES = {
projects: 'Your Projects (default)',
- stars: 'Starred Projects'
+ stars: 'Starred Projects',
+ project_activity: "Your Projects' Activity",
+ starred_project_activity: "Starred Projects' Activity"
}.with_indifferent_access.freeze
# Returns an Array usable by a select field for more user-friendly option text
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index ab9b068de05..a0220af4c30 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -43,24 +43,22 @@ module ProjectsHelper
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))
+ def project_title(project, name = nil, url = nil)
+ namespace_link =
+ if project.group
+ link_to(simple_sanitize(project.group.name), group_path(project.group))
+ else
+ owner = project.namespace.owner
+ link_to(simple_sanitize(owner.name), user_path(owner))
end
+
+ project_link = link_to(simple_sanitize(project.name), project_path(project))
+
+ full_title = namespace_link + ' / ' + project_link
+ full_title += ' &middot; '.html_safe + link_to(simple_sanitize(name), url) if name
+
+ content_tag :span do
+ full_title
end
end
@@ -158,8 +156,8 @@ module ProjectsHelper
end
end
- def repository_size(project = nil)
- "#{(project || @project).repository_size} MB"
+ def repository_size(project = @project)
+ "#{project.repository_size} MB"
rescue
# In order to prevent 500 error
# when application cannot allocate memory
@@ -298,7 +296,7 @@ module ProjectsHelper
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
- [@project.id, sha, "readme"].join('-')
+ [@project.path_with_namespace, sha, "readme"].join('-')
end
def round_commit_count(project)
@@ -315,6 +313,10 @@ module ProjectsHelper
end
end
+ def current_ref
+ @ref || @repository.try(:root_ref)
+ end
+
private
def filename_path(project, filename)
diff --git a/app/helpers/runners_helper.rb b/app/helpers/runners_helper.rb
new file mode 100644
index 00000000000..5d7d06c8490
--- /dev/null
+++ b/app/helpers/runners_helper.rb
@@ -0,0 +1,20 @@
+module RunnersHelper
+ def runner_status_icon(runner)
+ unless runner.contacted_at
+ return content_tag :i, nil,
+ class: "fa 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 fa-circle runner-status-#{status}",
+ title: "Runner is #{status}, last contact was #{time_ago_in_words(runner.contacted_at)} ago"
+ end
+end
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
new file mode 100644
index 00000000000..8142f733e76
--- /dev/null
+++ b/app/helpers/time_helper.rb
@@ -0,0 +1,27 @@
+module TimeHelper
+ 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
+
+
+ def date_from_to(from, to)
+ "#{from.to_s(:short)} - #{to.to_s(:short)}"
+ end
+end
diff --git a/app/helpers/triggers_helper.rb b/app/helpers/triggers_helper.rb
new file mode 100644
index 00000000000..2a3a7e80fca
--- /dev/null
+++ b/app/helpers/triggers_helper.rb
@@ -0,0 +1,5 @@
+module TriggersHelper
+ def ci_build_trigger_url(project_id, ref_name)
+ "#{Settings.gitlab_ci.url}/ci/api/v1/projects/#{project_id}/refs/#{ref_name}/trigger"
+ end
+end
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index f64d730b448..a674564c4ec 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -1,6 +1,6 @@
module VersionCheckHelper
def version_status_badge
- if Rails.env.production?
+ if Rails.env.production? && current_application_settings.version_check_enabled
image_tag VersionCheck.new.url
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/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..404842cf213
--- /dev/null
+++ b/app/mailers/ci/notify.rb
@@ -0,0 +1,46 @@
+module Ci
+ class Notify < ActionMailer::Base
+ include Ci::Emails::Builds
+
+ add_template_helper Ci::GitlabHelper
+
+ default_url_options[:host] = Gitlab.config.gitlab.host
+ default_url_options[:protocol] = Gitlab.config.gitlab.protocol
+ default_url_options[:port] = Gitlab.config.gitlab.port unless Gitlab.config.gitlab_on_standard_port?
+ default_url_options[:script_name] = Gitlab.config.gitlab.relative_url_root
+
+ default from: Gitlab.config.gitlab.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
+ # => #<Project id: 1, name: "Ruby on Rails", path: "ruby_on_rails", ...>
+ # >> 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/emails/notes.rb b/app/mailers/emails/notes.rb
index 63d4aca61af..87ba94a583d 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -12,7 +12,7 @@ module Emails
to: recipient(recipient_id),
subject: subject("#{@commit.title} (#{@commit.short_id})"))
- SentNotification.record(@commit, recipient_id, reply_key)
+ SentNotification.record_note(@note, recipient_id, reply_key)
end
def note_issue_email(recipient_id, note_id)
@@ -27,7 +27,7 @@ module Emails
to: recipient(recipient_id),
subject: subject("#{@issue.title} (##{@issue.iid})"))
- SentNotification.record(@issue, recipient_id, reply_key)
+ SentNotification.record_note(@note, recipient_id, reply_key)
end
def note_merge_request_email(recipient_id, note_id)
@@ -43,7 +43,7 @@ module Emails
to: recipient(recipient_id),
subject: subject("#{@merge_request.title} (##{@merge_request.iid})"))
- SentNotification.record(@merge_request, recipient_id, reply_key)
+ SentNotification.record_note(@note, recipient_id, reply_key)
end
end
end
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 4a6e18e6a74..caba63006da 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -50,10 +50,11 @@ module Emails
subject: subject("Invitation declined"))
end
- def project_was_moved_email(project_id, user_id)
+ def project_was_moved_email(project_id, user_id, old_path_with_namespace)
@current_user = @user = User.find user_id
@project = Project.find project_id
@target_url = namespace_project_url(@project.namespace, @project)
+ @old_path_with_namespace = old_path_with_namespace
mail(to: @user.notification_email,
subject: subject("Project was moved"))
end
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 5717c89e61d..50a409c3754 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -100,7 +100,7 @@ class Notify < BaseMailer
def mail_thread(model, headers = {})
if @project
- headers['X-GitLab-Project'] = @project.name
+ headers['X-GitLab-Project'] = @project.name
headers['X-GitLab-Project-Id'] = @project.id
headers['X-GitLab-Project-Path'] = @project.path_with_namespace
end
@@ -110,7 +110,7 @@ class Notify < BaseMailer
if reply_key
headers['X-GitLab-Reply-Key'] = reply_key
- address = Mail::Address.new(Gitlab::ReplyByEmail.reply_address(reply_key))
+ address = Mail::Address.new(Gitlab::IncomingEmail.reply_address(reply_key))
address.display_name = @project.name_with_namespace
headers['Reply-To'] = address
@@ -140,7 +140,7 @@ class Notify < BaseMailer
# * have a 'In-Reply-To' or 'References' header that references the original 'Message-ID'
#
def mail_answer_thread(model, headers = {})
- headers['Message-ID'] = SecureRandom.hex
+ headers['Message-ID'] = "<#{SecureRandom.hex}@#{Gitlab.config.gitlab.host}>"
headers['In-Reply-To'] = message_id(model)
headers['References'] = message_id(model)
@@ -150,6 +150,6 @@ class Notify < BaseMailer
end
def reply_key
- @reply_key ||= Gitlab::ReplyByEmail.reply_key
+ @reply_key ||= SentNotification.reply_key
end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index f8e5afa9b01..a020b24a550 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -149,6 +149,7 @@ class Ability
:admin_merge_request,
:create_merge_request,
:create_wiki,
+ :manage_builds,
:push_code
]
end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index c8c39db11bc..89b3116b9f2 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -1,9 +1,21 @@
+# == Schema Information
+#
+# Table name: abuse_reports
+#
+# id :integer not null, primary key
+# reporter_id :integer
+# user_id :integer
+# message :text
+# created_at :datetime
+# updated_at :datetime
+#
+
class AbuseReport < ActiveRecord::Base
- belongs_to :reporter, class_name: "User"
+ belongs_to :reporter, class_name: 'User'
belongs_to :user
validates :reporter, presence: true
validates :user, presence: true
validates :message, presence: true
- validates :user_id, uniqueness: { scope: :reporter_id }
+ validates :user_id, uniqueness: true
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8f27e35d723..c8841178e93 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -83,7 +83,7 @@ class ApplicationSetting < ActiveRecord::Base
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
restricted_signup_domains: Settings.gitlab['restricted_signup_domains'],
- import_sources: ['github','bitbucket','gitlab','gitorious','google_code','git']
+ import_sources: ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
)
end
diff --git a/app/models/ci/application_setting.rb b/app/models/ci/application_setting.rb
new file mode 100644
index 00000000000..0cf496f7d81
--- /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: Settings.gitlab_ci['all_broken_builds'],
+ add_pusher: 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..5d17f4418ed
--- /dev/null
+++ b/app/models/ci/build.rb
@@ -0,0 +1,309 @@
+# == 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 :runner, class_name: 'Ci::Runner'
+ belongs_to :trigger_request, class_name: 'Ci::TriggerRequest'
+ belongs_to :user
+
+ serialize :options
+
+ validates :commit, presence: true
+ validates :status, presence: true
+ validates :coverage, numericality: true, allow_blank: true
+ validates_presence_of :ref
+
+ 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]) }
+ scope :latest, ->() { where(id: unscope(:select).select('max(id)').group(:name, :ref)).order(stage_idx: :asc) }
+ scope :ignore_failures, ->() { where(allow_failure: false) }
+ scope :for_ref, ->(ref) { where(ref: ref) }
+ scope :similar, ->(build) { where(ref: build.ref, tag: build.tag, trigger_request_id: build.trigger_request_id) }
+
+ 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.ref = build.ref
+ new_build.tag = build.tag
+ 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.name = build.name
+ new_build.allow_failure = build.allow_failure
+ new_build.stage = build.stage
+ new_build.stage_idx = build.stage_idx
+ 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.should_create_next_builds?(build)
+ build.commit.create_next_builds(build.ref, build.tag, build.user, 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, :project, :gl_project,
+ to: :commit, prefix: false
+
+ def before_sha
+ Gitlab::Git::BLANK_SHA
+ end
+
+ def trace_html
+ html = Ci::Ansi2html::convert(trace) if trace.present?
+ html || ''
+ 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 project_recipients
+ recipients = project.email_recipients.split(' ')
+
+ if project.email_add_pusher? && user.present? && user.notification_email.present?
+ recipients << user.notification_email
+ end
+
+ recipients.uniq
+ 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
+ # if bad regex or something goes wrong we dont want to interrupt transition
+ # so we just silentrly ignore error for now
+ end
+ end
+
+ def raw_trace
+ if File.exist?(path_to_trace)
+ File.read(path_to_trace)
+ else
+ # backward compatibility
+ read_attribute :trace
+ end
+ end
+
+ def trace
+ trace = raw_trace
+ if project && trace.present?
+ trace.gsub(project.token, 'xxxxxx')
+ else
+ 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(
+ 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..fde754a92a1
--- /dev/null
+++ b/app/models/ci/commit.rb
@@ -0,0 +1,246 @@
+# == 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 :gl_project, class_name: '::Project', foreign_key: :gl_project_id
+ has_many :builds, dependent: :destroy, class_name: 'Ci::Build'
+ has_many :trigger_requests, dependent: :destroy, class_name: 'Ci::TriggerRequest'
+
+ validates_presence_of :sha
+ validate :valid_commit_sha
+
+ def self.truncate_sha(sha)
+ sha[0...8]
+ end
+
+ def to_param
+ sha
+ end
+
+ def project
+ @project ||= gl_project.ensure_gitlab_ci_project
+ end
+
+ def project_id
+ project.id
+ 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 git_author_name
+ commit_data.author_name if commit_data
+ end
+
+ def git_author_email
+ commit_data.author_email if commit_data
+ end
+
+ def git_commit_message
+ commit_data.message if commit_data
+ end
+
+ def short_sha
+ Ci::Commit.truncate_sha(sha)
+ end
+
+ def commit_data
+ @commit ||= gl_project.commit(sha)
+ rescue
+ nil
+ end
+
+ def stage
+ running_or_pending = builds_without_retry.running_or_pending
+ running_or_pending.limit(1).pluck(:stage).first
+ end
+
+ def create_builds(ref, tag, user, trigger_request = nil)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+ config_processor.stages.any? do |stage|
+ CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present?
+ end
+ end
+
+ def create_next_builds(ref, tag, user, trigger_request)
+ return if skip_ci? && trigger_request.blank?
+ return unless config_processor
+
+ stages = builds.where(ref: ref, tag: tag, trigger_request: trigger_request).group_by(&:stage)
+
+ config_processor.stages.any? do |stage|
+ unless stages.include?(stage)
+ CreateBuildsService.new.execute(self, stage, ref, tag, user, trigger_request).present?
+ end
+ end
+ end
+
+ def refs
+ builds.group(:ref).pluck(:ref)
+ end
+
+ def last_ref
+ builds.latest.first.try(:ref)
+ end
+
+ def builds_without_retry
+ builds.latest
+ end
+
+ def builds_without_retry_for_ref(ref)
+ builds.for_ref(ref).latest
+ 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 duration_for_ref(ref)
+ builds_without_retry_for_ref(ref).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_for_ref?(ref)
+ builds_without_retry_for_ref(ref).pluck(:id).size > 1
+ end
+
+ def config_processor
+ @config_processor ||= Ci::GitlabCiYamlProcessor.new(ci_yaml_file)
+ 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 ci_yaml_file
+ gl_project.repository.blob_at(sha, '.gitlab-ci.yml').data
+ rescue
+ nil
+ end
+
+ def skip_ci?
+ return false if builds.any?
+ git_commit_message =~ /(\[ci skip\])/ if git_commit_message
+ end
+
+ def update_committed!
+ update!(committed_at: DateTime.now)
+ end
+
+ def should_create_next_builds?(build)
+ # don't create other builds if this one is retried
+ other_builds = builds.similar(build).latest
+ return false unless other_builds.include?(build)
+
+ other_builds.all? do |build|
+ build.success? || build.ignored?
+ end
+ 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/project.rb b/app/models/ci/project.rb
new file mode 100644
index 00000000000..88ba933a434
--- /dev/null
+++ b/app/models/ci/project.rb
@@ -0,0 +1,215 @@
+# == 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
+
+ belongs_to :gl_project, class_name: '::Project', foreign_key: :gitlab_id
+
+ 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
+
+ delegate :name_with_namespace, :path_with_namespace, :web_url, :http_url_to_repo, :ssh_url_to_repo, to: :gl_project
+
+ #
+ # Validations
+ #
+ validates_presence_of :timeout, :token, :default_ref, :gitlab_id
+
+ validates_uniqueness_of :gitlab_id
+
+ validates :polling_interval,
+ presence: true,
+ if: ->(project) { project.always_build.present? }
+
+ 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 = {
+ gitlab_id: project.id,
+ default_ref: project.default_branch || 'master',
+ 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 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 gl_project_id, MAX(committed_at) committed_at FROM #{Ci::Commit.table_name} GROUP BY gl_project_id)"
+ joins("LEFT JOIN #{last_commit_subquery} AS last_commit ON #{Ci::Project.table_name}.gitlab_id = last_commit.gl_project_id").
+ order("CASE WHEN last_commit.committed_at IS NULL THEN 1 ELSE 0 END, last_commit.committed_at DESC")
+ end
+ end
+
+ def name
+ name_with_namespace
+ end
+
+ def path
+ path_with_namespace
+ end
+
+ def gitlab_url
+ web_url
+ 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?
+ self.default_ref ||= 'master'
+ 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}@"
+ http_url_to_repo.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`
+ 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 setup_finished?
+ commits.any?
+ end
+
+ def commits
+ gl_project.ci_commits
+ end
+
+ def builds
+ gl_project.ci_builds
+ 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..b66f1212f23
--- /dev/null
+++ b/app/models/ci/project_status.rb
@@ -0,0 +1,35 @@
+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
+
+ 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..6838ccfaaab
--- /dev/null
+++ b/app/models/ci/runner.rb
@@ -0,0 +1,84 @@
+# == 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(ci_runners.token) LIKE :query OR LOWER(ci_runners.description) like :query',
+ query: "%#{query.try(:downcase)}%")
+ end
+
+ def gl_projects_ids
+ projects.select(:gitlab_id)
+ 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..fe224b7dc70
--- /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::Project'
+ 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/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..8f03b0625da
--- /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::Project'
+
+ # 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/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 40642dc63ba..4db4ffb2e79 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -140,6 +140,12 @@ module Issuable
{
object_kind: self.class.name.underscore,
user: user.hook_attrs,
+ repository: {
+ name: project.name,
+ url: project.url_to_repo,
+ description: project.description,
+ homepage: project.web_url
+ },
object_attributes: hook_attrs
}
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 78f16c6304e..47600c57e35 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -27,6 +27,7 @@ class Event < ActiveRecord::Base
MERGED = 7
JOINED = 8 # User joined project
LEFT = 9 # User left project
+ DESTROYED = 10
delegate :name, :email, to: :author, prefix: true, allow_nil: true
delegate :title, to: :issue, prefix: true, allow_nil: true
@@ -48,6 +49,7 @@ class Event < ActiveRecord::Base
scope :code_push, -> { where(action: PUSHED) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids).recent }
scope :with_associations, -> { includes(project: :namespace) }
+ scope :for_milestone_id, ->(milestone_id) { where(target_type: "Milestone", target_id: milestone_id) }
class << self
def reset_event_cache_for(target)
@@ -71,7 +73,7 @@ class Event < ActiveRecord::Base
elsif created_project?
true
else
- (issue? || merge_request? || note? || milestone?) && target
+ ((issue? || merge_request? || note?) && target) || milestone?
end
end
@@ -115,6 +117,10 @@ class Event < ActiveRecord::Base
action == LEFT
end
+ def destroyed?
+ action == DESTROYED
+ end
+
def commented?
action == COMMENTED
end
@@ -124,7 +130,7 @@ class Event < ActiveRecord::Base
end
def created_project?
- created? && !target
+ created? && !target && target_type.nil?
end
def created_target?
@@ -180,6 +186,8 @@ class Event < ActiveRecord::Base
'joined'
elsif left?
'left'
+ elsif destroyed?
+ 'destroyed'
elsif commented?
"commented on"
elsif created_project?
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 9a8251bdad5..a078accbdbd 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -25,7 +25,7 @@ class WebHook < ActiveRecord::Base
default_value_for :note_events, false
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
- default_value_for :enable_ssl_verification, false
+ default_value_for :enable_ssl_verification, true
# HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 2456b7d0dc1..fc7e9abe29e 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -2,19 +2,20 @@
#
# Table name: issues
#
-# id :integer not null, primary key
-# title :string(255)
-# assignee_id :integer
-# author_id :integer
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# position :integer default(0)
-# branch_name :string(255)
-# description :text
-# milestone_id :integer
-# state :string(255)
-# iid :integer
+# id :integer not null, primary key
+# title :string(255)
+# assignee_id :integer
+# author_id :integer
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# position :integer default(0)
+# branch_name :string(255)
+# description :text
+# milestone_id :integer
+# state :string(255)
+# iid :integer
+# updated_by_id :integer
#
require 'carrierwave/orm/activerecord'
diff --git a/app/models/label.rb b/app/models/label.rb
index 230631b5180..4a22bd53400 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -24,7 +24,7 @@ class Label < ActiveRecord::Base
validates :color,
format: { with: /\A#[0-9A-Fa-f]{6}\Z/ },
allow_blank: false
- validates :project, presence: true
+ validates :project, presence: true, unless: Proc.new { |service| service.template? }
# Don't allow '?', '&', and ',' for label titles
validates :title,
@@ -34,6 +34,8 @@ class Label < ActiveRecord::Base
default_scope { order(title: :asc) }
+ scope :templates, -> { where(template: true) }
+
alias_attribute :name, :title
def self.reference_prefix
@@ -78,4 +80,8 @@ class Label < ActiveRecord::Base
def open_issues_count
issues.opened.count
end
+
+ def template?
+ template
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 467b90861f9..eb468c6cd53 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -19,6 +19,7 @@
# description :text
# position :integer default(0)
# locked_at :datetime
+# updated_by_id :integer
#
require Rails.root.join("app/models/commit")
@@ -432,10 +433,22 @@ class MergeRequest < ActiveRecord::Base
target_project.repository.fetch_ref(
source_project.repository.path_to_repo,
"refs/heads/#{source_branch}",
- "refs/merge-requests/#{iid}/head"
+ ref_path
)
end
+ def ref_path
+ "refs/merge-requests/#{iid}/head"
+ end
+
+ def ref_is_fetched?
+ File.exists?(File.join(project.repository.path_to_repo, ref_path))
+ end
+
+ def ensure_ref_fetched
+ fetch_ref unless ref_is_fetched?
+ end
+
def in_locked_state
begin
lock_mr
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index e317c8eac4d..c9ef8023aea 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -123,12 +123,12 @@ class MergeRequestDiff < ActiveRecord::Base
if new_diffs.any?
if new_diffs.size > Commit::DIFF_HARD_LIMIT_FILES
self.state = :overflow_diff_files_limit
- new_diffs = new_diffs.first[Commit::DIFF_HARD_LIMIT_LINES]
+ new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES)
end
if new_diffs.sum { |diff| diff.diff.lines.count } > Commit::DIFF_HARD_LIMIT_LINES
self.state = :overflow_diff_lines_limit
- new_diffs = new_diffs.first[Commit::DIFF_HARD_LIMIT_LINES]
+ new_diffs = new_diffs.first(Commit::DIFF_HARD_LIMIT_LINES)
end
end
@@ -144,12 +144,10 @@ class MergeRequestDiff < ActiveRecord::Base
# Collect array of Git::Diff objects
# between target and source branches
def unmerged_diffs
- diffs = compare_result.diffs
- diffs ||= []
- diffs
- rescue Gitlab::Git::Diff::TimeoutError => ex
+ compare_result.diffs || []
+ rescue Gitlab::Git::Diff::TimeoutError
self.state = :timeout
- diffs = []
+ []
end
def repository
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index c6aff6f709f..d979a35084b 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -61,7 +61,7 @@ class Milestone < ActiveRecord::Base
false
end
end
-
+
def open_items_count
self.issues.opened.count + self.merge_requests.opened.count
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 161a16ca61c..bc8525df5a5 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -137,7 +137,9 @@ class Namespace < ActiveRecord::Base
end
def send_update_instructions
- projects.each(&:send_move_instructions)
+ projects.each do |project|
+ project.send_move_instructions("#{path_was}/#{project.path}")
+ end
end
def kind
diff --git a/app/models/note.rb b/app/models/note.rb
index 36cad8f583d..de3b6df88f7 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -15,6 +15,7 @@
# noteable_id :integer
# system :boolean default(FALSE), not null
# st_diff :text
+# updated_by_id :integer
#
require 'carrierwave/orm/activerecord'
@@ -365,6 +366,6 @@ class Note < ActiveRecord::Base
end
def editable?
- !read_attribute(:system)
+ !system?
end
end
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 1395274173d..171b8df45c2 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -12,7 +12,7 @@ class Notification
class << self
def notification_levels
- [N_DISABLED, N_PARTICIPATING, N_WATCH, N_MENTION]
+ [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH]
end
def options_with_labels
@@ -26,7 +26,7 @@ class Notification
end
def project_notification_levels
- [N_DISABLED, N_PARTICIPATING, N_WATCH, N_GLOBAL, N_MENTION]
+ [N_DISABLED, N_MENTION, N_PARTICIPATING, N_WATCH, N_GLOBAL]
end
end
@@ -57,4 +57,21 @@ class Notification
def level
target.notification_level
end
+
+ def to_s
+ case level
+ when N_DISABLED
+ 'Disabled'
+ when N_PARTICIPATING
+ 'Participating'
+ when N_WATCH
+ 'Watching'
+ when N_MENTION
+ 'On mention'
+ when N_GLOBAL
+ 'Global'
+ else
+ # do nothing
+ end
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 69f9af91c51..bb47b9abb03 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -39,10 +39,13 @@ class Project < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Referable
include Sortable
+ include AfterCommitQueue
extend Gitlab::ConfigHelper
extend Enumerize
+ UNKNOWN_IMPORT_URL = 'http://unknown.git'
+
default_value_for :archived, false
default_value_for :visibility_level, gitlab_config_features.visibility_level
default_value_for :issues_enabled, gitlab_config_features.issues
@@ -73,6 +76,7 @@ class Project < ActiveRecord::Base
has_many :services
has_one :gitlab_ci_service, dependent: :destroy
has_one :campfire_service, dependent: :destroy
+ has_one :drone_ci_service, dependent: :destroy
has_one :emails_on_push_service, dependent: :destroy
has_one :irker_service, dependent: :destroy
has_one :pivotaltracker_service, dependent: :destroy
@@ -114,8 +118,11 @@ class Project < ActiveRecord::Base
has_many :deploy_keys, through: :deploy_keys_projects
has_many :users_star_projects, dependent: :destroy
has_many :starrers, through: :users_star_projects, source: :user
+ has_many :ci_commits, ->() { order('CASE WHEN ci_commits.committed_at IS NULL THEN 0 ELSE 1 END', :committed_at, :id) }, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
+ has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build'
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
+ has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -141,7 +148,7 @@ class Project < ActiveRecord::Base
validates_uniqueness_of :path, scope: :namespace_id
validates :import_url,
format: { with: /\A#{URI.regexp(%w(ssh git http https))}\z/, message: 'should be a valid url' },
- if: :import?
+ if: :external_import?
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_limit, on: :create
validate :avatar_type,
@@ -187,7 +194,7 @@ class Project < ActiveRecord::Base
state :finished
state :failed
- after_transition any => :started, do: :add_import_job
+ after_transition any => :started, do: :schedule_add_import_job
after_transition any => :finished, do: :clear_import_data
end
@@ -231,10 +238,10 @@ class Project < ActiveRecord::Base
return nil unless id.include?('/')
id = id.split('/')
- namespace = Namespace.find_by(path: id.first)
+ namespace = Namespace.by_path(id.first)
return nil unless namespace
- where(namespace_id: namespace.id).find_by(path: id.second)
+ where(namespace_id: namespace.id).where("LOWER(projects.path) = :path", path: id.second.downcase).first
end
def visibility_levels
@@ -271,8 +278,18 @@ class Project < ActiveRecord::Base
id && persisted?
end
+ def schedule_add_import_job
+ run_after_commit(:add_import_job)
+ end
+
def add_import_job
- RepositoryImportWorker.perform_in(2.seconds, id)
+ if forked?
+ unless RepositoryForkWorker.perform_async(id, forked_from_project.path_with_namespace, self.namespace.path)
+ import_fail
+ end
+ else
+ RepositoryImportWorker.perform_async(id)
+ end
end
def clear_import_data
@@ -280,6 +297,10 @@ class Project < ActiveRecord::Base
end
def import?
+ external_import? || forked?
+ end
+
+ def external_import?
import_url.present?
end
@@ -316,7 +337,7 @@ class Project < ActiveRecord::Base
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
@@ -392,7 +413,7 @@ class Project < ActiveRecord::Base
if template.nil?
# If no template, we should create an instance. Ex `create_gitlab_ci_service`
- service = self.send :"create_#{service_name}_service"
+ self.send :"create_#{service_name}_service"
else
Service.create_from_template(self.id, template)
end
@@ -400,12 +421,21 @@ class Project < ActiveRecord::Base
end
end
+ def create_labels
+ Label.templates.each do |label|
+ label = label.dup
+ label.template = nil
+ label.project_id = self.id
+ label.save
+ end
+ end
+
def find_service(list, name)
list.find { |service| service.to_param == name }
end
def gitlab_ci?
- gitlab_ci_service && gitlab_ci_service.active
+ gitlab_ci_service && gitlab_ci_service.active && gitlab_ci_project.present?
end
def ci_services
@@ -433,7 +463,7 @@ class Project < ActiveRecord::Base
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
@@ -451,8 +481,8 @@ class Project < ActiveRecord::Base
end
end
- def send_move_instructions
- NotificationService.new.project_was_moved(self)
+ def send_move_instructions(old_path_with_namespace)
+ NotificationService.new.project_was_moved(self, old_path_with_namespace)
end
def owner
@@ -594,7 +624,7 @@ class Project < ActiveRecord::Base
# So we basically we mute exceptions in next actions
begin
gitlab_shell.mv_repository("#{old_path_with_namespace}.wiki", "#{new_path_with_namespace}.wiki")
- send_move_instructions
+ send_move_instructions(old_path_with_namespace)
reset_events_cache
rescue
# Returning false does not rollback after_* transaction but gives
@@ -613,6 +643,7 @@ class Project < ActiveRecord::Base
name: name,
ssh_url: ssh_url_to_repo,
http_url: http_url_to_repo,
+ web_url: web_url,
namespace: namespace.name,
visibility_level: visibility_level
}
@@ -689,14 +720,8 @@ class Project < ActiveRecord::Base
end
def create_repository
- if forked?
- if gitlab_shell.fork_repository(forked_from_project.path_with_namespace, self.namespace.path)
- true
- else
- errors.add(:base, 'Failed to fork repository via gitlab-shell')
- false
- end
- else
+ # Forked import is handled asynchronously
+ unless forked?
if gitlab_shell.add_repository(path_with_namespace)
true
else
@@ -713,8 +738,26 @@ class Project < ActiveRecord::Base
def create_wiki
ProjectWiki.new(self, self.owner).wiki
true
- rescue ProjectWiki::CouldNotCreateWikiError => ex
+ rescue ProjectWiki::CouldNotCreateWikiError
errors.add(:base, 'Failed create wiki')
false
end
+
+ def ci_commit(sha)
+ ci_commits.find_by(sha: sha)
+ end
+
+ def ensure_ci_commit(sha)
+ ci_commit(sha) || ci_commits.create(sha: sha)
+ end
+
+ def ensure_gitlab_ci_project
+ gitlab_ci_project || create_gitlab_ci_project
+ end
+
+ def enable_ci
+ service = gitlab_ci_service || create_gitlab_ci_service
+ service.active = true
+ service.save
+ end
end
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 9e5da6f45d2..40058b53df5 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -69,14 +69,6 @@ class BuildkiteService < CiService
"#{project_url}/builds?commit=#{sha}"
end
- def builds_path
- "#{project_url}/builds?branch=#{project.default_branch}"
- end
-
- def status_img_path
- "#{buildkite_endpoint('badge')}/#{status_token}.svg"
- end
-
def title
'Buildkite'
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..cbf325cc525
--- /dev/null
+++ b/app/models/project_services/ci/hip_chat_message.rb
@@ -0,0 +1,73 @@
+module Ci
+ class HipChatMessage
+ include Gitlab::Application.routes.url_helpers
+
+ attr_reader :build
+
+ def initialize(build)
+ @build = build
+ end
+
+ def to_s
+ lines = Array.new
+ lines.push("<a href=\"#{ci_project_url(project)}\">#{project.name}</a> - ")
+ lines.push("<a href=\"#{ci_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}\">Commit ##{commit.id}</a></br>")
+ lines.push("#{commit.short_sha} #{commit.git_author_name} - #{commit.git_commit_message}</br>")
+ 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..0e6e97394bc
--- /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 < Ci::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..11a2743f969
--- /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 < Ci::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.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..5ac8907ecd0
--- /dev/null
+++ b/app/models/project_services/ci/slack_message.rb
@@ -0,0 +1,92 @@
+require 'slack-notifier'
+
+module Ci
+ class SlackMessage
+ include Gitlab::Application.routes.url_helpers
+
+ def initialize(commit)
+ @commit = commit
+ end
+
+ def pretext
+ ''
+ end
+
+ def color
+ attachment_color
+ end
+
+ def fallback
+ format(attachment_message)
+ end
+
+ def attachments
+ fields = []
+
+ commit.builds_without_retry.each do |build|
+ next if build.allow_failure?
+ next unless build.failed?
+ fields << {
+ title: build.name,
+ value: "Build <#{namespace_project_build_url(build.gl_project.namespace, build.gl_project, build)}|\##{build.id}> failed in #{build.duration.to_i} second(s)."
+ }
+ end
+
+ [{
+ text: attachment_message,
+ color: attachment_color,
+ fields: fields
+ }]
+ end
+
+ private
+
+ attr_reader :commit
+
+ def attachment_message
+ out = "<#{ci_project_url(project)}|#{project_name}>: "
+ out << "Commit <#{ci_namespace_project_commit_url(commit.gl_project.namespace, commit.gl_project, commit.sha)}|\##{commit.id}> "
+ 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..76db573dc17
--- /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 < Ci::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/ci_service.rb b/app/models/project_services/ci_service.rb
index 803402c83ee..88186113c68 100644
--- a/app/models/project_services/ci_service.rb
+++ b/app/models/project_services/ci_service.rb
@@ -25,12 +25,24 @@ class CiService < Service
def category
:ci
end
-
+
+ def valid_token?(token)
+ self.respond_to?(:token) && self.token.present? && self.token == token
+ end
+
def supported_events
%w(push)
end
- # Return complete url to build page
+ def merge_request_page(iid, sha, ref)
+ commit_page(sha, ref)
+ end
+
+ def commit_page(sha, ref)
+ build_page(sha, ref)
+ end
+
+ # Return complete url to merge_request page
#
# Ex.
# http://jenkins.example.com:8888/job/test1/scm/bySHA1/12d65c
@@ -45,10 +57,27 @@ class CiService < Service
#
#
# Ex.
- # @service.commit_status('13be4ac')
+ # @service.merge_request_status(9, '13be4ac', 'dev')
+ # # => 'success'
+ #
+ # @service.merge_request_status(10, '2abe4ac', 'dev)
+ # # => 'running'
+ #
+ #
+ def merge_request_status(iid, sha, ref)
+ commit_status(sha, ref)
+ end
+
+ # Return string with build status or :error symbol
+ #
+ # Allowed states: 'success', 'failed', 'running', 'pending', 'skipped'
+ #
+ #
+ # Ex.
+ # @service.commit_status('13be4ac', 'master')
# # => 'success'
#
- # @service.commit_status('2abe4ac')
+ # @service.commit_status('2abe4ac', 'dev')
# # => 'running'
#
#
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
new file mode 100644
index 00000000000..c73c4b058a1
--- /dev/null
+++ b/app/models/project_services/drone_ci_service.rb
@@ -0,0 +1,176 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+# push_events :boolean default(TRUE)
+# issues_events :boolean default(TRUE)
+# merge_requests_events :boolean default(TRUE)
+# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
+#
+
+class DroneCiService < CiService
+
+ prop_accessor :drone_url, :token, :enable_ssl_verification
+ validates :drone_url,
+ presence: true,
+ format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated?
+ validates :token,
+ presence: true,
+ if: :activated?
+
+ after_save :compose_service_hook, if: :activated?
+
+ def compose_service_hook
+ hook = service_hook || build_service_hook
+ hook.url = [drone_url, "/api/hook", "?owner=#{project.namespace.path}", "&name=#{project.path}", "&access_token=#{token}"].join
+ hook.enable_ssl_verification = enable_ssl_verification
+ hook.save
+ end
+
+ def execute(data)
+ case data[:object_kind]
+ when 'push'
+ service_hook.execute(data) if push_valid?(data)
+ when 'merge_request'
+ service_hook.execute(data) if merge_request_valid?(data)
+ when 'tag_push'
+ service_hook.execute(data) if tag_push_valid?(data)
+ end
+ end
+
+ def allow_target_ci?
+ true
+ end
+
+ def supported_events
+ %w(push merge_request tag_push)
+ end
+
+ def merge_request_status_path(iid, sha = nil, ref = nil)
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/pulls/#{iid}",
+ "?access_token=#{token}"]
+
+ URI.join(*url).to_s
+ end
+
+ def commit_status_path(sha, ref)
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/commits/#{sha}",
+ "?branch=#{URI::encode(ref.to_s)}&access_token=#{token}"]
+
+ URI.join(*url).to_s
+ end
+
+ def merge_request_status(iid, sha, ref)
+ response = HTTParty.get(merge_request_status_path(iid), verify: enable_ssl_verification)
+
+ if response.code == 200 and response['status']
+ case response['status']
+ when 'killed'
+ :canceled
+ when 'failure', 'error'
+ # Because drone return error if some test env failed
+ :failed
+ else
+ response["status"]
+ end
+ else
+ :error
+ end
+ rescue Errno::ECONNREFUSED
+ :error
+ end
+
+ def commit_status(sha, ref)
+ response = HTTParty.get(commit_status_path(sha, ref), verify: enable_ssl_verification)
+
+ if response.code == 200 and response['status']
+ case response['status']
+ when 'killed'
+ :canceled
+ when 'failure', 'error'
+ # Because drone return error if some test env failed
+ :failed
+ else
+ response["status"]
+ end
+ else
+ :error
+ end
+ rescue Errno::ECONNREFUSED
+ :error
+ end
+
+ def merge_request_page(iid, sha, ref)
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/redirect/pulls/#{iid}"]
+
+ URI.join(*url).to_s
+ end
+
+ def commit_page(sha, ref)
+ url = [drone_url,
+ "gitlab/#{project.namespace.path}/#{project.path}/redirect/commits/#{sha}",
+ "?branch=#{URI::encode(ref.to_s)}"]
+
+ URI.join(*url).to_s
+ end
+
+ def commit_coverage(sha, ref)
+ nil
+ end
+
+ def build_page(sha, ref)
+ commit_page(sha, ref)
+ end
+
+ def title
+ 'Drone CI'
+ end
+
+ def description
+ 'Drone is a Continuous Integration platform built on Docker, written in Go'
+ end
+
+ def to_param
+ 'drone_ci'
+ end
+
+ def fields
+ [
+ { type: 'text', name: 'token', placeholder: 'Drone CI project specific token' },
+ { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com' },
+ { type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
+ ]
+ end
+
+ private
+
+ def tag_push_valid?(data)
+ data[:total_commits_count] > 0 && !Gitlab::Git.blank_ref?(data[:after])
+ end
+
+ def push_valid?(data)
+ opened_merge_requests = project.merge_requests.opened.where(source_project_id: project.id,
+ source_branch: Gitlab::Git.ref_name(data[:ref]))
+
+ opened_merge_requests.empty? && data[:total_commits_count] > 0 &&
+ !Gitlab::Git.blank_ref?(data[:after])
+ end
+
+ def merge_request_valid?(data)
+ ['opened', 'reopened'].include?(data[:object_attributes][:state]) &&
+ data[:object_attributes][:merge_status] == 'unchecked'
+ end
+end
diff --git a/app/models/project_services/gitlab_ci_service.rb b/app/models/project_services/gitlab_ci_service.rb
index acbbc9935b6..4dcd16ede3a 100644
--- a/app/models/project_services/gitlab_ci_service.rb
+++ b/app/models/project_services/gitlab_ci_service.rb
@@ -19,25 +19,20 @@
#
class GitlabCiService < CiService
- API_PREFIX = "api/v1"
-
- prop_accessor :project_url, :token, :enable_ssl_verification
- validates :project_url,
- presence: true,
- format: { with: /\A#{URI.regexp(%w(http https))}\z/, message: "should be a valid url" }, if: :activated?
- validates :token,
- presence: true,
- format: { with: /\A([A-Za-z0-9]+)\z/ }, if: :activated?
+ include Gitlab::Application.routes.url_helpers
after_save :compose_service_hook, if: :activated?
+ after_save :ensure_gitlab_ci_project, if: :activated?
def compose_service_hook
hook = service_hook || build_service_hook
- hook.url = [project_url, "/build", "?token=#{token}"].join("")
- hook.enable_ssl_verification = enable_ssl_verification
hook.save
end
+ def ensure_gitlab_ci_project
+ project.ensure_gitlab_ci_project
+ end
+
def supported_events
%w(push tag_push)
end
@@ -45,81 +40,39 @@ class GitlabCiService < CiService
def execute(data)
return unless supported_events.include?(data[:object_kind])
- sha = data[:checkout_sha]
-
- if sha.present?
- file = ci_yaml_file(sha)
-
- if file && file.data
- data.merge!(ci_yaml_file: file.data)
- end
+ ci_project = project.gitlab_ci_project
+ if ci_project
+ current_user = User.find_by(id: data[:user_id])
+ Ci::CreateCommitService.new.execute(ci_project, current_user, data)
end
-
- service_hook.execute(data)
end
- def commit_status_path(sha, ref)
- URI::encode(project_url + "/refs/#{ref}/commits/#{sha}/status.json?token=#{token}")
+ def token
+ if project.gitlab_ci_project.present?
+ project.gitlab_ci_project.token
+ end
end
- def get_ci_build(sha, ref)
- @ci_builds ||= {}
- @ci_builds[sha] ||= HTTParty.get(commit_status_path(sha, ref), verify: false)
+ def get_ci_commit(sha, ref)
+ Ci::Project.find(project.gitlab_ci_project).commits.find_by_sha!(sha)
end
def commit_status(sha, ref)
- response = get_ci_build(sha, ref)
-
- if response.code == 200 and response["status"]
- response["status"]
- else
- :error
- end
- rescue Errno::ECONNREFUSED
+ get_ci_commit(sha, ref).status
+ rescue ActiveRecord::RecordNotFound
:error
end
- def fork_registration(new_project, private_token)
- params = {
- id: new_project.id,
- name_with_namespace: new_project.name_with_namespace,
- path_with_namespace: new_project.path_with_namespace,
- web_url: new_project.web_url,
- default_branch: new_project.default_branch,
- ssh_url_to_repo: new_project.ssh_url_to_repo
- }
-
- HTTParty.post(
- fork_registration_path,
- body: {
- project_id: project.id,
- project_token: token,
- private_token: private_token,
- data: params },
- verify: false
- )
- end
-
def commit_coverage(sha, ref)
- response = get_ci_build(sha, ref)
-
- if response.code == 200 and response["coverage"]
- response["coverage"]
- end
- rescue Errno::ECONNREFUSED
- nil
+ get_ci_commit(sha, ref).coverage
+ rescue ActiveRecord::RecordNotFound
+ :error
end
def build_page(sha, ref)
- URI::encode(project_url + "/refs/#{ref}/commits/#{sha}")
- end
-
- def builds_path
- project_url + "?ref=" + project.default_branch
- end
-
- def status_img_path
- project_url + "/status.png?ref=" + project.default_branch
+ if project.gitlab_ci_project.present?
+ ci_namespace_project_commit_url(project.namespace, project, sha)
+ end
end
def title
@@ -135,24 +88,6 @@ class GitlabCiService < CiService
end
def fields
- [
- { type: 'text', name: 'token', placeholder: 'GitLab CI project specific token' },
- { type: 'text', name: 'project_url', placeholder: 'http://ci.gitlabhq.com/projects/3' },
- { type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
- ]
- end
-
- private
-
- def ci_yaml_file(sha)
- repository.blob_at(sha, '.gitlab-ci.yml')
- end
-
- def fork_registration_path
- project_url.sub(/projects\/\d*/, "#{API_PREFIX}/forks")
- end
-
- def repository
- project.repository
+ []
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/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 7a15a861abc..af2840a57f0 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -85,17 +85,16 @@ class HipchatService < Service
def create_message(data)
object_kind = data[:object_kind]
- message = \
- case object_kind
- when "push", "tag_push"
- create_push_message(data)
- when "issue"
- create_issue_message(data) unless is_update?(data)
- when "merge_request"
- create_merge_request_message(data) unless is_update?(data)
- when "note"
- create_note_message(data)
- end
+ case object_kind
+ when "push", "tag_push"
+ create_push_message(data)
+ when "issue"
+ create_issue_message(data) unless is_update?(data)
+ when "merge_request"
+ create_merge_request_message(data) unless is_update?(data)
+ when "note"
+ create_note_message(data)
+ end
end
def create_push_message(push)
@@ -167,8 +166,6 @@ class HipchatService < Service
obj_attr = data[:object_attributes]
obj_attr = HashWithIndifferentAccess.new(obj_attr)
merge_request_id = obj_attr[:iid]
- source_branch = obj_attr[:source_branch]
- target_branch = obj_attr[:target_branch]
state = obj_attr[:state]
description = obj_attr[:description]
title = obj_attr[:title]
@@ -194,8 +191,6 @@ class HipchatService < Service
data = HashWithIndifferentAccess.new(data)
user_name = data[:user][:name]
- repo_attr = HashWithIndifferentAccess.new(data[:repository])
-
obj_attr = HashWithIndifferentAccess.new(data[:object_attributes])
note = obj_attr[:note]
note_url = obj_attr[: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/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index 36d9874edd3..7cd5e892507 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -34,6 +34,12 @@ class SlackService < Service
'slack'
end
+ def help
+ 'This service sends notifications to your Slack channel.<br/>
+ To setup this Service you need to create a new <b>"Incoming webhook"</b> in your Slack integration panel,
+ and enter the Webhook URL below.'
+ end
+
def fields
[
{ type: 'text', name: 'webhook',
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 56e49af2324..f602a965364 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -135,6 +135,10 @@ class ProjectTeam
!!find_member(user_id)
end
+ def human_max_access(user_id)
+ Gitlab::Access.options.key max_member_access(user_id)
+ end
+
def max_member_access(user_id)
access = []
access << project.project_members.find_by(user_id: user_id).try(:access_field)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 79b48ebfedf..2c5ab62d22c 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -549,7 +549,7 @@ class Repository
# Run GitLab post receive hook
post_receive_hook = Gitlab::Git::Hook.new('post-receive', path_to_repo)
- status = post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
+ post_receive_hook.trigger(gl_id, oldrev, newrev, ref)
else
# Remove tmp ref and return error to user
rugged.references.delete(tmp_ref)
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 460ca40be3f..3eed5c16e45 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -1,3 +1,17 @@
+# == Schema Information
+#
+# Table name: sent_notifications
+#
+# id :integer not null, primary key
+# project_id :integer
+# noteable_id :integer
+# noteable_type :string(255)
+# recipient_id :integer
+# commit_id :string(255)
+# line_code :string(255)
+# reply_key :string(255) not null
+#
+
class SentNotification < ActiveRecord::Base
belongs_to :project
belongs_to :noteable, polymorphic: true
@@ -8,13 +22,20 @@ class SentNotification < ActiveRecord::Base
validates :noteable_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
+ validates :line_code, format: { with: /\A[a-z0-9]+_\d+_\d+\Z/ }, allow_blank: true
class << self
+ def reply_key
+ return nil unless Gitlab::IncomingEmail.enabled?
+
+ SecureRandom.hex(16)
+ end
+
def for(reply_key)
find_by(reply_key: reply_key)
end
- def record(noteable, recipient_id, reply_key)
+ def record(noteable, recipient_id, reply_key, params = {})
return unless reply_key
noteable_id = nil
@@ -25,7 +46,7 @@ class SentNotification < ActiveRecord::Base
noteable_id = noteable.id
end
- create(
+ params.reverse_merge!(
project: noteable.project,
noteable_type: noteable.class.name,
noteable_id: noteable_id,
@@ -33,6 +54,14 @@ class SentNotification < ActiveRecord::Base
recipient_id: recipient_id,
reply_key: reply_key
)
+
+ create(params)
+ end
+
+ def record_note(note, recipient_id, reply_key, params = {})
+ params[:line_code] = note.line_code
+
+ record(note.noteable, recipient_id, reply_key, params)
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index dcef2866c3b..60fcc9d2857 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -135,6 +135,7 @@ class Service < ActiveRecord::Base
buildkite
campfire
custom_issue_tracker
+ drone_ci
emails_on_push
external_wiki
flowdock
diff --git a/app/models/user.rb b/app/models/user.rb
index 48e0e6ed48b..889d2d3b867 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -2,62 +2,59 @@
#
# Table name: users
#
-# id :integer not null, primary key
-# email :string(255) default(""), not null
-# encrypted_password :string(255) default(""), not null
-# reset_password_token :string(255)
-# reset_password_sent_at :datetime
-# remember_created_at :datetime
-# sign_in_count :integer default(0)
-# current_sign_in_at :datetime
-# last_sign_in_at :datetime
-# current_sign_in_ip :string(255)
-# last_sign_in_ip :string(255)
-# created_at :datetime
-# updated_at :datetime
-# name :string(255)
-# admin :boolean default(FALSE), not null
-# projects_limit :integer default(10)
-# skype :string(255) default(""), not null
-# linkedin :string(255) default(""), not null
-# twitter :string(255) default(""), not null
-# authentication_token :string(255)
-# theme_id :integer default(1), not null
-# bio :string(255)
-# failed_attempts :integer default(0)
-# locked_at :datetime
-# username :string(255)
-# can_create_group :boolean default(TRUE), not null
-# can_create_team :boolean default(TRUE), not null
-# state :string(255)
-# color_scheme_id :integer default(1), not null
-# notification_level :integer default(1), not null
-# password_expires_at :datetime
-# created_by_id :integer
-# last_credential_check_at :datetime
-# avatar :string(255)
-# confirmation_token :string(255)
-# confirmed_at :datetime
-# confirmation_sent_at :datetime
-# unconfirmed_email :string(255)
-# hide_no_ssh_key :boolean default(FALSE)
-# website_url :string(255) default(""), not null
-# github_access_token :string(255)
-# gitlab_access_token :string(255)
-# notification_email :string(255)
-# hide_no_password :boolean default(FALSE)
-# password_automatically_set :boolean default(FALSE)
-# bitbucket_access_token :string(255)
-# bitbucket_access_token_secret :string(255)
-# location :string(255)
-# encrypted_otp_secret :string(255)
-# encrypted_otp_secret_iv :string(255)
-# encrypted_otp_secret_salt :string(255)
-# otp_required_for_login :boolean default(FALSE), not null
-# otp_backup_codes :text
-# public_email :string(255) default(""), not null
-# dashboard :integer default(0)
-# project_view :integer default(0)
+# id :integer not null, primary key
+# email :string(255) default(""), not null
+# encrypted_password :string(255) default(""), not null
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime
+# updated_at :datetime
+# name :string(255)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
+# skype :string(255) default(""), not null
+# linkedin :string(255) default(""), not null
+# twitter :string(255) default(""), not null
+# authentication_token :string(255)
+# theme_id :integer default(1), not null
+# bio :string(255)
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# username :string(255)
+# can_create_group :boolean default(TRUE), not null
+# can_create_team :boolean default(TRUE), not null
+# state :string(255)
+# color_scheme_id :integer default(1), not null
+# notification_level :integer default(1), not null
+# password_expires_at :datetime
+# created_by_id :integer
+# last_credential_check_at :datetime
+# avatar :string(255)
+# confirmation_token :string(255)
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string(255)
+# hide_no_ssh_key :boolean default(FALSE)
+# website_url :string(255) default(""), not null
+# notification_email :string(255)
+# hide_no_password :boolean default(FALSE)
+# password_automatically_set :boolean default(FALSE)
+# location :string(255)
+# encrypted_otp_secret :string(255)
+# encrypted_otp_secret_iv :string(255)
+# encrypted_otp_secret_salt :string(255)
+# otp_required_for_login :boolean default(FALSE), not null
+# otp_backup_codes :text
+# public_email :string(255) default(""), not null
+# dashboard :integer default(0)
+# project_view :integer default(0)
+# layout :integer default(0)
#
require 'carrierwave/orm/activerecord'
@@ -133,6 +130,8 @@ class User < ActiveRecord::Base
has_many :assigned_issues, dependent: :destroy, foreign_key: :assignee_id, class_name: "Issue"
has_many :assigned_merge_requests, dependent: :destroy, foreign_key: :assignee_id, class_name: "MergeRequest"
has_many :oauth_applications, class_name: 'Doorkeeper::Application', as: :owner, dependent: :destroy
+ has_one :abuse_report, dependent: :destroy
+ has_many :ci_builds, dependent: :nullify, class_name: 'Ci::Build'
#
@@ -174,9 +173,12 @@ class User < ActiveRecord::Base
after_create :post_create_hook
after_destroy :post_destroy_hook
+ # User's Layout preference
+ enum layout: [:fixed, :fluid]
+
# User's Dashboard preference
# Note: When adding an option, it MUST go on the end of the array.
- enum dashboard: [:projects, :stars]
+ enum dashboard: [:projects, :stars, :project_activity, :starred_project_activity]
# User's Project preference
# Note: When adding an option, it MUST go on the end of the array.
@@ -331,6 +333,10 @@ class User < ActiveRecord::Base
@reset_token
end
+ def recently_sent_password_reset?
+ reset_password_sent_at.present? && reset_password_sent_at >= 1.minute.ago
+ end
+
def disable_two_factor!
update_attributes(
two_factor_enabled: false,
@@ -704,13 +710,7 @@ class User < ActiveRecord::Base
end
def manageable_namespaces
- @manageable_namespaces ||=
- begin
- namespaces = []
- namespaces << namespace
- namespaces += owned_groups
- namespaces += masters_groups
- end
+ @manageable_namespaces ||= [namespace] + owned_groups + masters_groups
end
def namespaces
@@ -757,4 +757,13 @@ class User < ActiveRecord::Base
def can_be_removed?
!solo_owned_groups.present?
end
+
+ def ci_authorized_projects
+ @ci_authorized_projects ||= Ci::Project.where(gitlab_id: authorized_projects)
+ end
+
+ def ci_authorized_runners
+ Ci::Runner.specific.includes(:runner_projects).
+ where(ci_runner_projects: { project_id: ci_authorized_projects } )
+ end
end
diff --git a/app/services/ci/create_builds_service.rb b/app/services/ci/create_builds_service.rb
new file mode 100644
index 00000000000..c420f3268fd
--- /dev/null
+++ b/app/services/ci/create_builds_service.rb
@@ -0,0 +1,27 @@
+module Ci
+ class CreateBuildsService
+ def execute(commit, stage, ref, tag, user, trigger_request)
+ builds_attrs = commit.config_processor.builds_for_stage_and_ref(stage, ref, tag)
+
+ builds_attrs.map do |build_attrs|
+ # don't create the same build twice
+ unless commit.builds.find_by(ref: ref, tag: tag, trigger_request: trigger_request, name: build_attrs[:name])
+ build_attrs.slice!(:name,
+ :commands,
+ :tag_list,
+ :options,
+ :allow_failure,
+ :stage,
+ :stage_idx)
+
+ build_attrs.merge!(ref: ref,
+ tag: tag,
+ trigger_request: trigger_request,
+ user: user)
+
+ commit.builds.create!(build_attrs)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/create_commit_service.rb b/app/services/ci/create_commit_service.rb
new file mode 100644
index 00000000000..fc1ae5774d5
--- /dev/null
+++ b/app/services/ci/create_commit_service.rb
@@ -0,0 +1,26 @@
+module Ci
+ class CreateCommitService
+ def execute(project, user, params)
+ 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
+
+ tag = origin_ref.start_with?('refs/tags/')
+ commit = project.gl_project.ensure_ci_commit(sha)
+ commit.update_committed!
+ commit.create_builds(ref, tag, user)
+
+ commit
+ 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..4b86cb0a1f5
--- /dev/null
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -0,0 +1,22 @@
+module Ci
+ class CreateTriggerRequestService
+ def execute(project, trigger, ref, variables = nil)
+ commit = project.gl_project.commit(ref)
+ return unless commit
+
+ # check if ref is tag
+ tag = project.gl_project.repository.find_tag(ref).present?
+
+ ci_commit = project.gl_project.ensure_ci_commit(commit.sha)
+
+ trigger_request = trigger.trigger_requests.create!(
+ variables: variables,
+ commit: ci_commit,
+ )
+
+ if ci_commit.create_builds(ref, tag, nil, 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..71b61bbe389
--- /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.joins(commit: { gl_project: :gitlab_ci_project }).where(ci_projects: { shared_runners_enabled: true })
+ else
+ # do run projects which are only assigned to this runner
+ builds.joins(:commit).where(ci_commits: { gl_project_id: current_runner.gl_projects_ids })
+ 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..92e6df442b4
--- /dev/null
+++ b/app/services/ci/web_hook_service.rb
@@ -0,0 +1,35 @@
+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,
+ before_sha: build.before_sha,
+ sha: build.sha,
+ })
+ end
+ end
+end
diff --git a/app/services/compare_service.rb b/app/services/compare_service.rb
index 70f642baaaa..bfe6a3dc4be 100644
--- a/app/services/compare_service.rb
+++ b/app/services/compare_service.rb
@@ -4,7 +4,10 @@ require 'securerandom'
# and return Gitlab::CompareResult object that responds to commits and diffs
class CompareService
def execute(source_project, source_branch, target_project, target_branch)
- source_sha = source_project.commit(source_branch).sha
+ source_commit = source_project.commit(source_branch)
+ return unless source_commit
+
+ source_sha = source_commit.sha
# If compare with other project we need to fetch ref first
unless target_project == source_project
diff --git a/app/services/event_create_service.rb b/app/services/event_create_service.rb
index 103d6b0a08b..07fc77001a5 100644
--- a/app/services/event_create_service.rb
+++ b/app/services/event_create_service.rb
@@ -46,6 +46,10 @@ class EventCreateService
create_record_event(milestone, current_user, Event::REOPENED)
end
+ def destroy_milestone(milestone, current_user)
+ create_record_event(milestone, current_user, Event::DESTROYED)
+ end
+
def leave_note(note, current_user)
create_record_event(note, current_user, Event::COMMENTED)
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 7aecee217d8..008833eed80 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -21,7 +21,7 @@ module Files
create_target_branch
end
- if sha = commit
+ if commit
success
else
error("Something went wrong. Your changes were not committed")
diff --git a/app/services/files/create_service.rb b/app/services/files/create_service.rb
index 91d715b2d63..ffbb5993279 100644
--- a/app/services/files/create_service.rb
+++ b/app/services/files/create_service.rb
@@ -19,10 +19,12 @@ module Files
end
unless project.empty_repo?
+ @file_path.slice!(0) if @file_path.start_with?('/')
+
blob = repository.blob_at_branch(@current_branch, @file_path)
if blob
- raise_error("Your changes could not be committed, because file with such name exists")
+ raise_error("Your changes could not be committed because a file with the same name already exists")
end
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index 0a73244774a..f9a8265d2d4 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -55,6 +55,12 @@ class GitPushService
@push_data = build_push_data(oldrev, newrev, ref)
+ # If CI was disabled but .gitlab-ci.yml file was pushed
+ # we enable CI automatically
+ if !project.gitlab_ci? && gitlab_ci_yaml?(newrev)
+ project.enable_ci
+ end
+
EventCreateService.new.push(project, user, @push_data)
project.execute_hooks(@push_data.dup, :push_hooks)
project.execute_services(@push_data.dup, :push_hooks)
@@ -143,4 +149,10 @@ class GitPushService
def commit_user(commit)
commit.author || user
end
+
+ def gitlab_ci_yaml?(sha)
+ @project.repository.blob_at(sha, '.gitlab-ci.yml')
+ rescue Rugged::ReferenceError
+ nil
+ end
end
diff --git a/app/services/merge_requests/merge_service.rb b/app/services/merge_requests/merge_service.rb
index 98a67c0bc99..fcc0f2a6a8d 100644
--- a/app/services/merge_requests/merge_service.rb
+++ b/app/services/merge_requests/merge_service.rb
@@ -38,6 +38,10 @@ module MergeRequests
}
repository.merge(current_user, merge_request.source_sha, merge_request.target_branch, options)
+ rescue Exception => e
+ merge_request.update(merge_error: "Something went wrong during merge")
+ Rails.logger.error(e.message)
+ return false
end
def after_merge
diff --git a/app/services/milestones/destroy_service.rb b/app/services/milestones/destroy_service.rb
new file mode 100644
index 00000000000..2414966505b
--- /dev/null
+++ b/app/services/milestones/destroy_service.rb
@@ -0,0 +1,27 @@
+module Milestones
+ class DestroyService < Milestones::BaseService
+ def execute(milestone)
+
+ Milestone.transaction do
+ update_params = { milestone: nil }
+
+ milestone.issues.each do |issue|
+ Issues::UpdateService.new(project, current_user, update_params).execute(issue)
+ end
+
+ milestone.merge_requests.each do |merge_request|
+ MergeRequests::UpdateService.new(project, current_user, update_params).execute(merge_request)
+ end
+
+ event_service.destroy_milestone(milestone, current_user)
+
+ Event.for_milestone_id(milestone.id).each do |event|
+ event.target_id = nil
+ event.save
+ end
+
+ milestone.destroy
+ end
+ end
+ end
+end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index e294b23bc23..a6b22348650 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -183,12 +183,12 @@ class NotificationService
mailer.group_access_granted_email(group_member.id)
end
- def project_was_moved(project)
+ def project_was_moved(project, old_path_with_namespace)
recipients = project.team.members
recipients = reject_muted_users(recipients, project)
recipients.each do |recipient|
- mailer.project_was_moved_email(project.id, recipient.id)
+ mailer.project_was_moved_email(project.id, recipient.id, old_path_with_namespace)
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index b35aed005da..faf1ee008e7 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -55,16 +55,14 @@ module Projects
@project.save
if @project.persisted? && !@project.import?
- unless @project.create_repository
- raise 'Failed to create repository'
- end
+ raise 'Failed to create repository' unless @project.create_repository
end
end
after_create_actions if @project.persisted?
@project
- rescue => ex
+ rescue
@project.errors.add(:base, "Can't save project. Please try again later")
@project
end
@@ -87,6 +85,8 @@ module Projects
@project.build_missing_services
+ @project.create_labels
+
event_service.create_project(@project, current_user)
system_hook_service.execute_hooks_for(@project, :create)
diff --git a/app/services/projects/download_service.rb b/app/services/projects/download_service.rb
new file mode 100644
index 00000000000..99f22293d0d
--- /dev/null
+++ b/app/services/projects/download_service.rb
@@ -0,0 +1,43 @@
+module Projects
+ class DownloadService < BaseService
+
+ WHITELIST = [
+ /^[^.]+\.fogbugz.com$/
+ ]
+
+ def initialize(project, url)
+ @project, @url = project, url
+ end
+
+ def execute
+ return nil unless valid_url?(@url)
+
+ uploader = FileUploader.new(@project)
+ uploader.download!(@url)
+ uploader.store!
+
+ filename = uploader.image? ? uploader.file.basename : uploader.file.filename
+
+ {
+ 'alt' => filename,
+ 'url' => uploader.secure_url,
+ 'is_image' => uploader.image?
+ }
+ end
+
+ private
+
+ def valid_url?(url)
+ url && http?(url) && valid_domain?(url)
+ end
+
+ def http?(url)
+ url =~ /\A#{URI::regexp(['http', 'https'])}\z/
+ end
+
+ def valid_domain?(url)
+ host = URI.parse(url).host
+ WHITELIST.any? { |entry| entry === host }
+ end
+ end
+end
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index 50f208b11d1..46374a3909a 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -18,7 +18,13 @@ module Projects
if new_project.persisted?
if @project.gitlab_ci?
- ForkRegistrationWorker.perform_async(@project.id, new_project.id, @current_user.private_token)
+ new_project.enable_ci
+
+ settings = @project.gitlab_ci_project.attributes.select do |attr_name, value|
+ ["public", "shared_runners_enabled", "allow_git_fetch"].include? attr_name
+ end
+
+ new_project.gitlab_ci_project.update(settings)
end
end
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 550ed6897dd..c327c244f0d 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -38,7 +38,7 @@ module Projects
project.save!
# Notifications
- project.send_move_instructions
+ project.send_move_instructions(old_path)
# Move main repository
unless gitlab_shell.mv_repository(old_path, new_path)
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 330b8bbf9d2..a36ae0b766c 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -118,6 +118,11 @@
.col-sm-10
= f.text_area :sign_in_text, class: 'form-control', rows: 4
.help-block Markdown enabled
+ .form-group
+ = f.label :help_page_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :help_page_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
.form-actions
= f.submit 'Save', class: 'btn btn-primary'
diff --git a/app/views/admin/applications/index.html.haml b/app/views/admin/applications/index.html.haml
index fc921a966f3..f8cd98f0ec4 100644
--- a/app/views/admin/applications/index.html.haml
+++ b/app/views/admin/applications/index.html.haml
@@ -2,7 +2,7 @@
%h3.page-title
System OAuth applications
%p.light
- System OAuth application does not belong to certain user and can be managed only by admins
+ System OAuth applications don't belong to any user and can only be managed by admins
%hr
%p= link_to 'New Application', new_admin_application_path, class: 'btn btn-success'
%table.table.table-striped
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 54191aadda6..8657d2c71fe 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -58,7 +58,7 @@
%p
Reply by email
%span.light.pull-right
- = boolean_to_icon Gitlab::ReplyByEmail.enabled?
+ = boolean_to_icon Gitlab::IncomingEmail.enabled?
.col-md-4
%h4
Components
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
new file mode 100644
index 00000000000..ad58a3837f6
--- /dev/null
+++ b/app/views/admin/labels/_form.html.haml
@@ -0,0 +1,35 @@
+= form_for [:admin, @label], html: { class: 'form-horizontal label-form js-requires-input' } do |f|
+ -if @label.errors.any?
+ .row
+ .col-sm-offset-2.col-sm-10
+ .alert.alert-danger
+ - @label.errors.full_messages.each do |msg|
+ %span= msg
+ %br
+
+ .form-group
+ = f.label :title, class: 'control-label'
+ .col-sm-10
+ = f.text_field :title, class: "form-control", required: true
+ .form-group
+ = f.label :color, "Background Color", class: 'control-label'
+ .col-sm-10
+ .input-group
+ .input-group-addon.label-color-preview &nbsp;
+ = f.color_field :color, class: "form-control"
+ .help-block
+ Choose any color.
+ %br
+ Or you can choose one of suggested colors below
+
+ .suggest-colors
+ - suggested_colors.each do |color|
+ = link_to '#', style: "background-color: #{color}", data: { color: color } do
+ &nbsp;
+
+ .form-actions
+ = f.submit 'Save', class: 'btn btn-save js-save-button'
+ = link_to "Cancel", admin_labels_path, class: 'btn btn-cancel'
+
+:coffeescript
+ new Labels
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
new file mode 100644
index 00000000000..596e06243dd
--- /dev/null
+++ b/app/views/admin/labels/_label.html.haml
@@ -0,0 +1,5 @@
+%li{id: dom_id(label)}
+ = render_colored_label(label)
+ .pull-right
+ = link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
+ = link_to 'Remove', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Remove this label? Are you sure?"}
diff --git a/app/views/admin/labels/destroy.js.haml b/app/views/admin/labels/destroy.js.haml
new file mode 100644
index 00000000000..9d51762890f
--- /dev/null
+++ b/app/views/admin/labels/destroy.js.haml
@@ -0,0 +1,2 @@
+- if @labels.size == 0
+ $('.labels').load(document.URL + ' .light-well').hide().fadeIn(1000)
diff --git a/app/views/admin/labels/edit.html.haml b/app/views/admin/labels/edit.html.haml
new file mode 100644
index 00000000000..45c62a76259
--- /dev/null
+++ b/app/views/admin/labels/edit.html.haml
@@ -0,0 +1,9 @@
+- page_title "Edit", @label.name, "Labels"
+%h3
+ Edit label
+ %span.light #{@label.name}
+.back-link
+ = link_to admin_labels_path do
+ &larr; To labels list
+%hr
+= render 'form'
diff --git a/app/views/admin/labels/index.html.haml b/app/views/admin/labels/index.html.haml
new file mode 100644
index 00000000000..d67454c03e7
--- /dev/null
+++ b/app/views/admin/labels/index.html.haml
@@ -0,0 +1,16 @@
+- page_title "Labels"
+= link_to new_admin_label_path, class: "pull-right btn btn-new" do
+ New label
+%h3.page-title
+ Labels
+%hr
+
+.labels
+ - if @labels.present?
+ %ul.bordered-list.manage-labels-list
+ = render @labels
+ = paginate @labels, theme: 'gitlab'
+ - else
+ .light-well
+ .nothing-here-block There are no labels yet
+
diff --git a/app/views/admin/labels/new.html.haml b/app/views/admin/labels/new.html.haml
new file mode 100644
index 00000000000..8d298ad20f7
--- /dev/null
+++ b/app/views/admin/labels/new.html.haml
@@ -0,0 +1,7 @@
+- page_title "New Label"
+%h3 New label
+.back-link
+ = link_to admin_labels_path do
+ &larr; To labels list
+%hr
+= render 'form'
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index 9d5e934c8ba..4245d0f1eda 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -6,6 +6,8 @@
%span.cred (Admin)
.pull-right
+ - unless @user == current_user
+ = link_to 'Log in as this user', login_as_admin_user_path(@user), method: :post, class: "btn btn-grouped btn-info"
= link_to edit_admin_user_path(@user), class: "btn btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 5e40d95d1c5..e3698ac1c46 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -97,5 +97,5 @@
- if user.access_locked?
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: "btn btn-xs btn-success", data: { confirm: 'Are you sure?' }
- if user.can_be_removed?
- = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All tickets linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
+ = link_to 'Destroy', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Maybe block the user instead? Are you sure?" }, method: :delete, class: "btn btn-xs btn-remove"
= paginate @users, theme: "gitlab"
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..2df58713214
--- /dev/null
+++ b/app/views/ci/admin/builds/_build.html.haml
@@ -0,0 +1,34 @@
+- gl_project = build.project.gl_project
+- if build.commit && build.project
+ %tr.build
+ %td.build-link
+ = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
+ %strong #{build.id}
+
+ %td.status
+ = ci_status_with_icon(build.status)
+
+ %td.commit-link
+ = link_to ci_status_path(build.commit) do
+ %strong #{build.commit.short_sha}
+
+ %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..d23119162cc
--- /dev/null
+++ b/app/views/ci/admin/builds/index.html.haml
@@ -0,0 +1,28 @@
+%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
+
+ - @builds.each do |build|
+ = render "ci/admin/builds/build", build: build
+
+= 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..a342d6e1cf0
--- /dev/null
+++ b/app/views/ci/admin/projects/_project.html.haml
@@ -0,0 +1,29 @@
+- last_commit = project.commits.last
+%tr
+ %td
+ = project.id
+ %td
+ = link_to [:ci, project] do
+ %strong= project.name
+ %td
+ - if last_commit
+ = ci_status_with_icon(last_commit.status)
+ - if project.last_commit_date
+ &middot;
+ = time_ago_in_words project.last_commit_date
+ ago
+ - else
+ No builds yet
+ %td
+ - if project.public
+ %i.fa.fa-globe
+ Public
+ - else
+ %i.fa.fa-lock
+ Private
+ %td
+ = project.commits.count
+ %td
+ = link_to [:ci, :admin, project], method: :delete, class: 'btn btn-danger btn-sm' do
+ %i.fa.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..dc7b041473b
--- /dev/null
+++ b/app/views/ci/admin/projects/index.html.haml
@@ -0,0 +1,15 @@
+%table.table
+ %thead
+ %tr
+ %th ID
+ %th Name
+ %th Last build
+ %th Access
+ %th Builds
+ %th
+
+ - @projects.each do |project|
+ = render "ci/admin/projects/project", project: project
+
+= 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'
+ &nbsp;
+ - 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..b9d6703ff41
--- /dev/null
+++ b/app/views/ci/admin/runners/index.html.haml
@@ -0,0 +1,52 @@
+%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, id: 'runners-search', class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token'
+ = 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
+
+ - @runners.each do |runner|
+ = render "ci/admin/runners/runner", runner: runner
+= 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..5bb442cbf92
--- /dev/null
+++ b/app/views/ci/admin/runners/show.html.haml
@@ -0,0 +1,124 @@
+= 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), id: 'runner-projects-search', 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 Build ID
+ %th Status
+ %th Project
+ %th Commit
+ %th Finished at
+
+ - @builds.each do |build|
+ %tr.build
+ %td.id
+ - gl_project = build.project.gl_project
+ = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
+ = build.id
+
+ %td.status
+ = ci_status_with_icon(build.status)
+
+ %td.status
+ = build.project.name
+
+ %td.build-link
+ = link_to ci_status_path(build.commit) do
+ %strong #{build.commit.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/commits/_commit.html.haml b/app/views/ci/commits/_commit.html.haml
new file mode 100644
index 00000000000..b24a3b826cf
--- /dev/null
+++ b/app/views/ci/commits/_commit.html.haml
@@ -0,0 +1,33 @@
+%tr.build
+ %td.status
+ = ci_status_with_icon(commit.status)
+ - if commit.running?
+ &middot;
+ = commit.stage
+
+
+ %td.build-link
+ = link_to ci_status_path(commit) do
+ %strong #{commit.short_sha}
+
+ %td.build-message
+ %span= truncate_first_line(commit.git_commit_message)
+
+ %td.build-branch
+ - unless @ref
+ %span
+ - commit.refs.each do |ref|
+ = link_to truncate(ref, length: 25), ci_project_path(@project, ref: 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/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/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
new file mode 100644
index 00000000000..e2179e60f3e
--- /dev/null
+++ b/app/views/ci/lints/_create.html.haml
@@ -0,0 +1,39 @@
+- if @status
+ %p
+ %b Status:
+ syntax is correct
+ %i.fa.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.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..a9b954771c5
--- /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.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..69689a75022
--- /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.ref}
+%p
+ Message: #{@build.commit.git_commit_message}
+
+%p
+ Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_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..6de5dc10f17
--- /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.ref %>
+Message: <%= @build.commit.git_commit_message %>
+
+Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_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..4e3015a356b
--- /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.ref}
+%p
+ Message: #{@build.commit.git_commit_message}
+
+%p
+ Url: #{link_to @build.short_sha, namespace_project_build_url(@build.gl_project.namespace, @build.gl_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..d0a43ae1c12
--- /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.ref %>
+Message: <%= @build.commit.git_commit_message %>
+
+Url: <%= namespace_project_build_url(@build.gl_project.namespace, @build.gl_project, @build) %>
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/show.html.haml b/app/views/ci/projects/show.html.haml
new file mode 100644
index 00000000000..888b1ea41d5
--- /dev/null
+++ b/app/views/ci/projects/show.html.haml
@@ -0,0 +1,60 @@
+= render 'ci/shared/guide' unless @project.setup_finished?
+
+- if current_user && can?(current_user, :manage_project, gl_project) && !@project.any_runners?
+ .alert.alert-danger
+ Builds for this project wont be served unless you configure runners on
+ = link_to "Runners page", runners_path(@project.gl_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)
+
+ %li.pull-right
+ = link_to 'Go to project', project_path(gl_project), class: 'btn btn-sm'
+
+- 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.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/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
+ &larr; 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'
+ &nbsp;
+ - 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..db2d7f2f4b6
--- /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', runners_path(@project.gl_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/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/dashboard/_activities.html.haml b/app/views/dashboard/_activities.html.haml
index 213b5d65b3c..19d919f9b6a 100644
--- a/app/views/dashboard/_activities.html.haml
+++ b/app/views/dashboard/_activities.html.haml
@@ -1,13 +1,13 @@
.hidden-xs
= render "events/event_last_push", event: @last_push
+.gray-content-block
- if current_user
%ul.nav.nav-pills.event_filter.pull-right
%li.pull-right
- = link_to dashboard_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do
+ = link_to dashboard_projects_path(:atom, { private_token: current_user.private_token }), class: 'rss-btn' do
%i.fa.fa-rss
-
= render 'shared/event_filter'
- %hr
+
.content_list
= spinner
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
new file mode 100644
index 00000000000..9f4be025bf2
--- /dev/null
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -0,0 +1,7 @@
+%ul.center-top-menu
+ %li{ class: ("active" unless params[:filter]) }
+ = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
+ Your Projects
+ %li{ class: ("active" if params[:filter] == 'starred') }
+ = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
+ Starred Projects
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 8a397a84e0e..64bd356f546 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -1,7 +1,7 @@
%ul.center-top-menu
- = nav_link(page: [dashboard_groups_path]) do
+ = nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups', data: {placement: 'right'} do
Your Groups
- = nav_link(page: [explore_groups_path]) do
- = link_to explore_groups_path, title: 'Explore groups', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = nav_link(page: explore_groups_path) do
+ = link_to explore_groups_path, title: 'Explore groups', data: {placement: 'bottom'} do
Explore Groups
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index f7be194c696..ed480b8caf8 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -1,10 +1,10 @@
%ul.center-top-menu
- = nav_link(path: ['dashboard#show', 'root#show']) do
- = link_to dashboard_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
+ = nav_link(path: ['projects#index', 'root#index']) do
+ = link_to dashboard_projects_path, title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
Your Projects
= nav_link(page: starred_dashboard_projects_path) do
= link_to starred_dashboard_projects_path, title: 'Starred Projects', data: {placement: 'right'} do
Starred Projects
- = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path]) do
- = link_to explore_root_path, title: 'Explore', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = nav_link(page: [explore_root_path, trending_explore_projects_path, starred_explore_projects_path, explore_projects_path], html_options: { class: 'hidden-xs' }) do
+ = link_to explore_root_path, title: 'Explore', data: {placement: 'right'} do
Explore Projects
diff --git a/app/views/dashboard/_snippets_head.html.haml b/app/views/dashboard/_snippets_head.html.haml
new file mode 100644
index 00000000000..0ae62d6f1b6
--- /dev/null
+++ b/app/views/dashboard/_snippets_head.html.haml
@@ -0,0 +1,7 @@
+%ul.center-top-menu
+ = nav_link(page: dashboard_snippets_path, html_options: {class: 'home'}) do
+ = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
+ Your Snippets
+ = nav_link(page: explore_snippets_path) do
+ = link_to explore_snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
+ Explore Snippets
diff --git a/app/views/dashboard/activity.html.haml b/app/views/dashboard/activity.html.haml
index 7a5a093add5..aa57df14c23 100644
--- a/app/views/dashboard/activity.html.haml
+++ b/app/views/dashboard/activity.html.haml
@@ -1,6 +1,11 @@
= content_for :meta_tags do
- if current_user
- = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+ = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+
+- page_title "Activity"
+- header_title "Activity", activity_dashboard_path
+
+= render 'dashboard/activity_head'
%section.activities
= render 'activities'
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index fbe523b4b66..c249f5cacec 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -1,14 +1,17 @@
- page_title "Groups"
+- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
-.slead
- Group members have access to all group projects.
+.gray-content-block
- if current_user.can_create_group?
%span.pull-right.hidden-xs
- = link_to new_group_path, class: "btn btn-new btn-sm" do
+ = link_to new_group_path, class: "btn btn-new" do
%i.fa.fa-plus
New Group
-%ul.bordered-list
+ .title Welcome to the groups!
+ Group members have access to all group projects.
+
+%ul.content-list
- @group_members.each do |group_member|
- group = group_member.group
= render 'shared/groups/group', group: group, group_member: group_member
diff --git a/app/views/dashboard/issues.html.haml b/app/views/dashboard/issues.html.haml
index 94318d1bcf5..cd602e897b7 100644
--- a/app/views/dashboard/issues.html.haml
+++ b/app/views/dashboard/issues.html.haml
@@ -1,21 +1,17 @@
- page_title "Issues"
+- header_title "Issues", issues_dashboard_path(assignee_id: current_user.id)
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, issues_dashboard_url(format: :atom, private_token: current_user.private_token), title: "#{current_user.name} issues")
-%h3.page-title
- Issues
-
-%p.light
- List all issues from all projects you have access to.
-%hr
.append-bottom-20
.pull-right
- if current_user
- .hidden-xs.pull-left
- = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: 'btn' do
+ .hidden-xs.pull-left.prepend-top-20
+ = link_to issues_dashboard_url(format: :atom, private_token: current_user.private_token), class: '' do
%i.fa.fa-rss
= render 'shared/issuable/filter', type: :issues
+
= render 'shared/issues'
diff --git a/app/views/dashboard/merge_requests.html.haml b/app/views/dashboard/merge_requests.html.haml
index 90611d562b0..d1f332fa0d3 100644
--- a/app/views/dashboard/merge_requests.html.haml
+++ b/app/views/dashboard/merge_requests.html.haml
@@ -1,11 +1,6 @@
- page_title "Merge Requests"
-%h3.page-title
- Merge Requests
+- header_title "Merge Requests", merge_requests_dashboard_path(assignee_id: current_user.id)
-
-%p.light
- List all merge requests from all projects you have access to.
-%hr
.append-bottom-20
= render 'shared/issuable/filter', type: :merge_requests
= render 'shared/merge_requests'
diff --git a/app/views/dashboard/milestones/_milestone.html.haml b/app/views/dashboard/milestones/_milestone.html.haml
index d6f3e029a38..55080d6b3fe 100644
--- a/app/views/dashboard/milestones/_milestone.html.haml
+++ b/app/views/dashboard/milestones/_milestone.html.haml
@@ -1,20 +1,22 @@
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- %h4
- = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
+ .row
+ .col-sm-6
+ %strong
+ = link_to_gfm truncate(milestone.title, length: 100), dashboard_milestone_path(milestone.safe_title, title: milestone.title)
+ .col-sm-6
+ .pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to issues_dashboard_path(milestone_title: milestone.title) do
= pluralize milestone.issue_count, 'Issue'
- &nbsp;
+ &middot;
= link_to merge_requests_dashboard_path(milestone_title: milestone.title) do
= pluralize milestone.merge_requests_count, 'Merge Request'
- &nbsp;
- %span.light #{milestone.percent_complete}% complete
-
.col-sm-6
= milestone_progress_bar(milestone)
- %div
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name_with_namespace
+ .row
+ .col-sm-6
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = milestone.project.name_with_namespace
diff --git a/app/views/dashboard/milestones/index.html.haml b/app/views/dashboard/milestones/index.html.haml
index 9a9a5e139a4..21b25c3986e 100644
--- a/app/views/dashboard/milestones/index.html.haml
+++ b/app/views/dashboard/milestones/index.html.haml
@@ -1,21 +1,19 @@
- page_title "Milestones"
-%h3.page-title
- Milestones
- %span.pull-right #{@dashboard_milestones.count} milestones
+- header_title "Milestones", dashboard_milestones_path
-%p.light
- List all milestones from all projects you have access to.
-
-%hr
= render 'shared/milestones_filter'
+
+.gray-content-block
+ .oneline
+ List all milestones from all projects you have access to.
+
.milestones
- .panel.panel-default
- %ul.well-list
- - if @dashboard_milestones.blank?
- %li
- .nothing-here-block No milestones to show
- - else
- - @dashboard_milestones.each do |milestone|
- = render 'milestone', milestone: milestone
+ %ul.content-list
+ - if @dashboard_milestones.blank?
+ %li
+ .nothing-here-block No milestones to show
+ - else
+ - @dashboard_milestones.each do |milestone|
+ = render 'milestone', milestone: milestone
= paginate @dashboard_milestones, theme: "gitlab"
diff --git a/app/views/dashboard/_projects.html.haml b/app/views/dashboard/projects/_projects.html.haml
index ef9b9ce756a..e09e032a7f1 100644
--- a/app/views/dashboard/_projects.html.haml
+++ b/app/views/dashboard/projects/_projects.html.haml
@@ -4,7 +4,7 @@
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control'
- if current_user.can_create_project?
%span.input-group-btn
- = link_to new_project_path, class: 'btn btn-success' do
+ = link_to new_project_path, class: 'btn btn-green' do
New project
- = render 'shared/projects/list', projects: @projects
+ = render 'shared/projects/list', projects: @projects, ci: true
diff --git a/app/views/dashboard/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index 4e7d6639727..4e7d6639727 100644
--- a/app/views/dashboard/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
diff --git a/app/views/dashboard/show.atom.builder b/app/views/dashboard/projects/index.atom.builder
index e9a612231d5..d2c51486841 100644
--- a/app/views/dashboard/show.atom.builder
+++ b/app/views/dashboard/projects/index.atom.builder
@@ -1,9 +1,9 @@
xml.instruct!
xml.feed "xmlns" => "http://www.w3.org/2005/Atom", "xmlns:media" => "http://search.yahoo.com/mrss/" do
xml.title "Activity"
- xml.link href: dashboard_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
- xml.link href: dashboard_url, rel: "alternate", type: "text/html"
- xml.id dashboard_url
+ xml.link href: dashboard_projects_url(format: :atom, private_token: current_user.try(:private_token)), rel: "self", type: "application/atom+xml"
+ xml.link href: dashboard_projects_url, rel: "alternate", type: "text/html"
+ xml.id dashboard_projects_url
xml.updated @events.maximum(:updated_at).strftime("%Y-%m-%dT%H:%M:%SZ") if @events.any?
@events.each do |event|
diff --git a/app/views/dashboard/show.html.haml b/app/views/dashboard/projects/index.html.haml
index 4cf2feb9aa6..7a16b811f6b 100644
--- a/app/views/dashboard/show.html.haml
+++ b/app/views/dashboard/projects/index.html.haml
@@ -1,6 +1,9 @@
= content_for :meta_tags do
- if current_user
- = auto_discovery_link_tag(:atom, dashboard_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+ = auto_discovery_link_tag(:atom, dashboard_projects_url(format: :atom, private_token: current_user.private_token), title: "All activity")
+
+- page_title "Projects"
+- header_title "Projects", root_path
= render 'dashboard/projects_head'
diff --git a/app/views/dashboard/projects/starred.html.haml b/app/views/dashboard/projects/starred.html.haml
index 19f3975e530..f75f2e0a32a 100644
--- a/app/views/dashboard/projects/starred.html.haml
+++ b/app/views/dashboard/projects/starred.html.haml
@@ -1,24 +1,13 @@
- page_title "Starred Projects"
-= render 'dashboard/projects_head'
-
-- if @projects.any?
- = render 'shared/show_aside'
+- header_title "Projects", projects_path
- .dashboard.row
- %section.activities.col-md-7
- = render 'dashboard/activities'
- %aside.col-md-5
- .panel.panel-default.projects-list-holder
- .panel-heading.clearfix
- .input-group
- = search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control'
- - if current_user.can_create_project?
- %span.input-group-btn
- = link_to new_project_path, class: 'btn btn-success' do
- New project
+= render 'dashboard/projects_head'
- = render 'shared/projects/list', projects: @projects, projects_limit: 20
+- if @last_push
+ = render "events/event_last_push", event: @last_push
+- if @projects.any?
+ = render 'projects'
- else
%h3 You don't have starred projects yet
%p.slead Visit project page and press on star icon and it will appear on this page.
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
new file mode 100644
index 00000000000..d3908062f43
--- /dev/null
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -0,0 +1,38 @@
+- page_title "Snippets"
+- header_title "Snippets", dashboard_snippets_path
+
+= render 'dashboard/snippets_head'
+
+.gray-content-block
+ .pull-right
+ = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
+ Add new snippet
+
+ .oneline
+ Share code pastes with others out of git repository
+
+%ul.nav.nav-tabs.prepend-top-20
+ = nav_tab :scope, nil do
+ = link_to dashboard_snippets_path do
+ All
+ %span.badge
+ = current_user.snippets.count
+ = nav_tab :scope, 'are_private' do
+ = link_to dashboard_snippets_path(scope: 'are_private') do
+ Private
+ %span.badge
+ = current_user.snippets.are_private.count
+ = nav_tab :scope, 'are_internal' do
+ = link_to dashboard_snippets_path(scope: 'are_internal') do
+ Internal
+ %span.badge
+ = current_user.snippets.are_internal.count
+ = nav_tab :scope, 'are_public' do
+ = link_to dashboard_snippets_path(scope: 'are_public') do
+ Public
+ %span.badge
+ = current_user.snippets.are_public.count
+
+.my-snippets
+ = render 'snippets/snippets'
+
diff --git a/app/views/devise/passwords/new.html.haml b/app/views/devise/passwords/new.html.haml
index 29ffe8a8be3..535e85869e5 100644
--- a/app/views/devise/passwords/new.html.haml
+++ b/app/views/devise/passwords/new.html.haml
@@ -6,7 +6,7 @@
.devise-errors
= devise_error_messages!
.clearfix.append-bottom-20
- = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email]
+ = f.email_field :email, placeholder: "Email", class: "form-control", required: true, value: params[:user_email], autofocus: true
.clearfix
= f.submit "Reset password", class: "btn-primary btn"
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
new file mode 100644
index 00000000000..4974bb7f7fb
--- /dev/null
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -0,0 +1,9 @@
+= form_tag(user_omniauth_authorize_path("crowd"), id: 'new_crowd_user' ) do
+ = text_field_tag :username, nil, {class: "form-control top", placeholder: "Username", autofocus: "autofocus"}
+ = password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
+ - if devise_mapping.rememberable?
+ .remember-me.checkbox
+ %label{for: "remember_me"}
+ = check_box_tag :remember_me, '1', false, id: 'remember_me'
+ %span Remember me
+ = button_tag "Sign in", class: "btn-save btn" \ No newline at end of file
diff --git a/app/views/devise/shared/_signin_box.html.haml b/app/views/devise/shared/_signin_box.html.haml
index bb5e479697d..41ad2c231d4 100644
--- a/app/views/devise/shared/_signin_box.html.haml
+++ b/app/views/devise/shared/_signin_box.html.haml
@@ -8,15 +8,21 @@
.login-body
- if form_based_providers.any?
%ul.nav.nav-tabs
+ - if crowd_enabled?
+ %li.active
+ = link_to "Crowd", "#tab-crowd", 'data-toggle' => 'tab'
- @ldap_servers.each_with_index do |server, i|
- %li{class: (:active if i.zero?)}
+ %li{class: (:active if i.zero? && !crowd_enabled?)}
= link_to server['label'], "#tab-#{server['provider_name']}", 'data-toggle' => 'tab'
- if signin_enabled?
%li
= link_to 'Standard', '#tab-signin', 'data-toggle' => 'tab'
.tab-content
+ - if crowd_enabled?
+ %div.tab-pane.active{id: "tab-crowd"}
+ = render 'devise/sessions/new_crowd'
- @ldap_servers.each_with_index do |server, i|
- %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero?)}
+ %div.tab-pane{id: "tab-#{server['provider_name']}", class: (:active if i.zero? && !crowd_enabled?)}
= render 'devise/sessions/new_ldap', server: server
- if signin_enabled?
%div#tab-signin.tab-pane
diff --git a/app/views/events/_commit.html.haml b/app/views/events/_commit.html.haml
index 742b74a67c7..ad63841ccf3 100644
--- a/app/views/events/_commit.html.haml
+++ b/app/views/events/_commit.html.haml
@@ -1,5 +1,5 @@
%li.commit
.commit-row-title
= link_to truncate_sha(commit[:id]), namespace_project_commit_path(project.namespace, project, commit[:id]), class: "commit_short_id", alt: ''
- &nbsp;
+ &middot;
= gfm event_commit_title(commit[:message]), project: project
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 0faab4458e9..9aacc79d686 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -3,8 +3,8 @@
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
- = cache [event, "v1"] do
- = image_tag avatar_icon(event.author_email, 24), class: "avatar s24", alt:''
+ = cache [event, "v2.1"] do
+ = image_tag avatar_icon(event.author_email, 46), class: "avatar s46", alt:''
- if event.created_project?
= render "events/event/created_project", event: event
- elsif event.push?
diff --git a/app/views/events/_event_last_push.html.haml b/app/views/events/_event_last_push.html.haml
index 6a0c6cba41b..ffc37ad6178 100644
--- a/app/views/events/_event_last_push.html.haml
+++ b/app/views/events/_event_last_push.html.haml
@@ -1,14 +1,14 @@
- if show_last_push_widget?(event)
- .event-last-push
- .event-last-push-text
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
- %strong= event.ref_name
- %span at
- %strong= link_to_project event.project
- #{time_ago_with_tooltip(event.created_at)}
+ .gray-content-block.clear-block
+ .event-last-push
+ .event-last-push-text
+ %span You pushed to
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ %strong= event.ref_name
+ %span at
+ %strong= link_to_project event.project
+ #{time_ago_with_tooltip(event.created_at)}
- .pull-right
- = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
- Create Merge Request
- %hr
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
+ Create Merge Request
diff --git a/app/views/events/event/_common.html.haml b/app/views/events/event/_common.html.haml
index a39e62e9dac..4ecf1c33d2a 100644
--- a/app/views/events/event/_common.html.haml
+++ b/app/views/events/event/_common.html.haml
@@ -5,13 +5,14 @@
- if event.target
%strong= link_to "##{event.target_iid}", [event.project.namespace.becomes(Namespace), event.project, event.target]
- at
+
+ = event_preposition(event)
- if event.project
= link_to_project event.project
- else
= event.project_name
-
+
- if event.target.respond_to?(:title)
.event-body
.event-note
diff --git a/app/views/events/event/_note.html.haml b/app/views/events/event/_note.html.haml
index 07bec1697f5..830fec0b4ab 100644
--- a/app/views/events/event/_note.html.haml
+++ b/app/views/events/event/_note.html.haml
@@ -4,7 +4,7 @@
= event.action_name
= event_note_title_html(event)
at
-
+
- if event.project
= link_to_project event.project
- else
@@ -13,7 +13,6 @@
.event-body
.event-note
.md
- %i.fa.fa-comment-o.event-note-icon
= event_note(event.target.note, project: event.project)
- note = event.target
- if note.attachment.url
diff --git a/app/views/explore/_head.html.haml b/app/views/explore/_head.html.haml
new file mode 100644
index 00000000000..d8a57560788
--- /dev/null
+++ b/app/views/explore/_head.html.haml
@@ -0,0 +1,6 @@
+.explore-title
+ %h3
+ Explore GitLab
+ %p.lead
+ Discover projects, groups and snippets. Share your projects with others
+ %br
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 80acb914365..83d4d321c83 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -1,14 +1,19 @@
-- page_title "Groups"
+- page_title "Groups"
+- header_title "Groups", dashboard_groups_path
+
- if current_user
= render 'dashboard/groups_head'
-.clearfix.append-bottom-10
+- else
+ = render 'explore/head'
+
+.gray-content-block.clearfix
.pull-left
= form_tag explore_groups_path, method: :get, class: 'form-inline form-tiny' do |f|
= hidden_field_tag :sort, @sort
.form-group
= search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "groups_search"
.form-group
- = button_tag 'Search', class: "btn btn-primary wide"
+ = button_tag 'Search', class: "btn btn-default"
.pull-right
.dropdown.inline
@@ -30,7 +35,7 @@
= link_to explore_groups_path(sort: sort_value_oldest_updated) do
= sort_title_oldest_updated
-%ul.bordered-list
+%ul.content-list
- @groups.each do |group|
= render 'shared/groups/group', group: group
- unless @groups.present?
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index 4b91291caf4..5a3d689d1e5 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -3,7 +3,7 @@
.form-group
= search_field_tag :search, params[:search], placeholder: "Filter by name", class: "form-control search-text-input", id: "projects_search"
.form-group
- = button_tag 'Search', class: "btn btn-primary wide"
+ = button_tag 'Search', class: "btn btn-success"
.pull-right.hidden-sm.hidden-xs
- if current_user
diff --git a/app/views/explore/projects/index.html.haml b/app/views/explore/projects/index.html.haml
index 0cfdf5cfd15..67e38ca3127 100644
--- a/app/views/explore/projects/index.html.haml
+++ b/app/views/explore/projects/index.html.haml
@@ -1,8 +1,12 @@
-- page_title "Projects"
+- page_title "Projects"
+- header_title "Projects", root_path
+
- if current_user
= render 'dashboard/projects_head'
-.clearfix
+- else
+ = render 'explore/head'
+
+.gray-content-block.clearfix
= render 'filter'
-%br
= render 'projects', projects: @projects
= paginate @projects, theme: "gitlab"
diff --git a/app/views/explore/projects/starred.html.haml b/app/views/explore/projects/starred.html.haml
index 4a9fcae4bed..596cb0a96cd 100644
--- a/app/views/explore/projects/starred.html.haml
+++ b/app/views/explore/projects/starred.html.haml
@@ -1,11 +1,17 @@
-- page_title "Starred Projects"
+- page_title "Projects"
+- header_title "Projects", root_path
+
- if current_user
= render 'dashboard/projects_head'
+- else
+ = render 'explore/head'
+
.explore-trending-block
- .lead
- %i.fa.fa-star
- See most starred projects
+ .gray-content-block
.pull-right
= render 'explore/projects/dropdown'
+ .oneline
+ %i.fa.fa-star
+ See most starred projects
= render 'projects', projects: @starred_projects
= paginate @starred_projects, theme: 'gitlab'
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index 4c7e7d44733..5ea6d81c5b9 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -1,16 +1,16 @@
-- page_title "Trending Projects"
+- page_title "Projects"
+- header_title "Projects", root_path
+
- if current_user
= render 'dashboard/projects_head'
-.explore-title
- %h3
- Explore GitLab
- %p.lead
- Discover projects and groups. Share your projects with others
-%hr
+- else
+ = render 'explore/head'
+
.explore-trending-block
- .lead
- %i.fa.fa-comments-o
- See most discussed projects for last month
+ .gray-content-block
.pull-right
= render 'explore/projects/dropdown'
+ .oneline
+ %i.fa.fa-comments-o
+ See most discussed projects for last month
= render 'projects', projects: @trending_projects
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
new file mode 100644
index 00000000000..7e4fa7d4873
--- /dev/null
+++ b/app/views/explore/snippets/index.html.haml
@@ -0,0 +1,18 @@
+- page_title "Snippets"
+- header_title "Snippets", snippets_path
+
+- if current_user
+ = render 'dashboard/snippets_head'
+- else
+ = render 'explore/head'
+
+.gray-content-block
+ - if current_user
+ .pull-right
+ = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
+ Add new snippet
+
+ .oneline
+ Public snippets created by you and other users are listed here
+
+= render 'snippets/snippets'
diff --git a/app/views/groups/_projects.html.haml b/app/views/groups/_projects.html.haml
index b2e32ced5e0..2b27a88794d 100644
--- a/app/views/groups/_projects.html.haml
+++ b/app/views/groups/_projects.html.haml
@@ -4,7 +4,7 @@
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control'
- if can? current_user, :create_projects, @group
%span.input-group-btn
- = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-success' do
+ = link_to new_project_path(namespace_id: @group.id), class: 'btn btn-green' do
New project
- = render 'shared/projects/list', projects: @projects, projects_limit: 20
+ = render 'shared/projects/list', projects: @projects, projects_limit: 20, stars: false
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 2ff4b7e23ea..ae8fc9f85f0 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,3 +1,6 @@
+- header_title group_title(@group, "Settings", edit_group_path(@group))
+- @blank_container = true
+
.panel.panel-default
.panel-heading
%strong= @group.name
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index dba395cc8fa..3a6d07ebddf 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -1,15 +1,13 @@
- page_title "Members"
+- header_title group_title(@group, "Members", group_group_members_path(@group))
- show_roles = should_user_see_group_roles?(current_user, @group)
-%h3.page-title
- Group members
- if show_roles
%p.light
Members of group have access to all group projects.
Read more about permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-%hr
.clearfix.js-toggle-container
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index f0d90782556..08d97e418a3 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,25 +1,24 @@
- page_title "Issues"
+- header_title group_title(@group, "Issues", issues_group_path(@group))
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, issues_group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} issues")
-%h3.page-title
- Issues
-%p.light
- Only issues from
- %strong #{@group.name}
- group are listed here.
- - if current_user
- To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
-%hr
-.append-bottom-20
+= render 'shared/issuable/filter', type: :issues
+.gray-content-block.second-block
.pull-right
- if current_user
.hidden-xs.pull-left
- = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token), class: 'btn' do
+ = link_to issues_group_url(@group, format: :atom, private_token: current_user.private_token) do
%i.fa.fa-rss
+ %div
+ Only issues from
+ %strong #{@group.name}
+ group are listed here.
+ - if current_user
+ To see all issues you should visit #{link_to 'dashboard', issues_dashboard_path} page.
- = render 'shared/issuable/filter', type: :issues
-= render 'shared/issues'
+.prepend-top-default
+ = render 'shared/issues'
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index ca85a158707..425ad8331bf 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,14 +1,13 @@
- page_title "Merge Requests"
-%h3.page-title
- Merge Requests
+- header_title group_title(@group, "Merge Requests", merge_requests_group_path(@group))
-%p.light
- Only merge requests from
- %strong #{@group.name}
- group are listed here.
- - if current_user
- To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
-%hr
-.append-bottom-20
- = render 'shared/issuable/filter', type: :merge_requests
-= render 'shared/merge_requests'
+= render 'shared/issuable/filter', type: :merge_requests
+.gray-content-block.second-block
+ %div
+ Only merge requests from
+ %strong #{@group.name}
+ group are listed here.
+ - if current_user
+ To see all merge requests you should visit #{link_to 'dashboard', merge_requests_dashboard_path} page.
+.prepend-top-default
+ = render 'shared/merge_requests'
diff --git a/app/views/groups/milestones/_header_title.html.haml b/app/views/groups/milestones/_header_title.html.haml
new file mode 100644
index 00000000000..d7fabf53587
--- /dev/null
+++ b/app/views/groups/milestones/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title group_title(@group, "Milestones", group_milestones_path(@group))
diff --git a/app/views/groups/milestones/_milestone.html.haml b/app/views/groups/milestones/_milestone.html.haml
index ba30e6e07c6..41dffdd2fb8 100644
--- a/app/views/groups/milestones/_milestone.html.haml
+++ b/app/views/groups/milestones/_milestone.html.haml
@@ -1,25 +1,29 @@
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone.milestones.first) }
- .pull-right
- - if can?(current_user, :admin_group, @group)
- - if milestone.closed?
- = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-sm btn-grouped btn-reopen"
- - else
- = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-sm btn-close"
- %h4
- = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
+ .row
+ .col-sm-6
+ %strong
+ = link_to_gfm truncate(milestone.title, length: 100), group_milestone_path(@group, milestone.safe_title, title: milestone.title)
+ .col-sm-6
+ .pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to issues_group_path(@group, milestone_title: milestone.title) do
= pluralize milestone.issue_count, 'Issue'
- &nbsp;
+ &middot;
= link_to merge_requests_group_path(@group, milestone_title: milestone.title) do
= pluralize milestone.merge_requests_count, 'Merge Request'
- &nbsp;
- %span.light #{milestone.percent_complete}% complete
.col-sm-6
= milestone_progress_bar(milestone)
- %div
- - milestone.milestones.each do |milestone|
- = link_to milestone_path(milestone) do
- %span.label.label-gray
- = milestone.project.name
+ .row
+ .col-sm-6
+ %div
+ - milestone.milestones.each do |milestone|
+ = link_to milestone_path(milestone) do
+ %span.label.label-gray
+ = milestone.project.name
+ .col-sm-6
+ - if can?(current_user, :admin_group, @group)
+ - if milestone.closed?
+ = link_to 'Reopen Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :activate }), method: :put, class: "btn btn-xs btn-grouped btn-reopen"
+ - else
+ = link_to 'Close Milestone', group_milestone_path(@group, milestone.safe_title, title: milestone.title, milestone: {state_event: :close }), method: :put, class: "btn btn-xs btn-close"
diff --git a/app/views/groups/milestones/index.html.haml b/app/views/groups/milestones/index.html.haml
index 385222fa5b7..2bbcad5fdfb 100644
--- a/app/views/groups/milestones/index.html.haml
+++ b/app/views/groups/milestones/index.html.haml
@@ -1,23 +1,17 @@
- page_title "Milestones"
-%h3.page-title
- Milestones
- %span.pull-right #{@group_milestones.count} milestones
+- header_title group_title(@group, "Milestones", group_milestones_path(@group))
-%p.light
+= render 'shared/milestones_filter'
+.gray-content-block
Only milestones from
%strong #{@group.name}
group are listed here.
-
-%hr
-
-= render 'shared/milestones_filter'
.milestones
- .panel.panel-default
- %ul.well-list
- - if @group_milestones.blank?
- %li
- .nothing-here-block No milestones to show
- - else
- - @group_milestones.each do |milestone|
- = render 'milestone', milestone: milestone
+ %ul.content-list
+ - if @group_milestones.blank?
+ %li
+ .nothing-here-block No milestones to show
+ - else
+ - @group_milestones.each do |milestone|
+ = render 'milestone', milestone: milestone
= paginate @group_milestones, theme: "gitlab"
diff --git a/app/views/groups/milestones/show.html.haml b/app/views/groups/milestones/show.html.haml
index 8f2decb851f..0c213f42186 100644
--- a/app/views/groups/milestones/show.html.haml
+++ b/app/views/groups/milestones/show.html.haml
@@ -1,4 +1,6 @@
- page_title @group_milestone.title, "Milestones"
+= render "header_title"
+
%h4.page-title
.issue-box{ class: "issue-box-#{@group_milestone.closed? ? 'closed' : 'open'}" }
- if @group_milestone.closed?
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index d06cfa7ff9f..f1d507a50c7 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -1,4 +1,6 @@
- page_title "Projects"
+- header_title group_title(@group, "Projects", projects_group_path(@group))
+
.panel.panel-default
.panel-heading
%strong= @group.name
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 0577f4ec142..a9ba9d2ba10 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -1,3 +1,6 @@
+- unless can?(current_user, :read_group, @group)
+ - @disable_search_panel = true
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, group_url(@group, format: :atom, private_token: current_user.private_token), title: "#{@group.name} activity")
@@ -16,22 +19,25 @@
= render 'shared/show_aside'
- .row
- %section.activities.col-md-7
- .hidden-xs
- - if current_user
- = render "events/event_last_push", event: @last_push
-
+ - if can?(current_user, :read_group, @group)
+ .row
+ %section.activities.col-md-7
+ .hidden-xs
- if current_user
+ = render "events/event_last_push", event: @last_push
+
%ul.nav.nav-pills.event_filter.pull-right
%li
= link_to group_path(@group, { format: :atom, private_token: current_user.private_token }), title: "Feed", class: 'rss-btn' do
%i.fa.fa-rss
- = render 'shared/event_filter'
- %hr
+ = render 'shared/event_filter'
+ %hr
- .content_list
- = spinner
- %aside.side.col-md-5
- = render "projects", projects: @projects
+ .content_list
+ = spinner
+ %aside.side.col-md-5
+ = render "projects", projects: @projects
+ - else
+ %p
+ This group does not have public projects
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index bf4b7234b21..57bc91ea5a9 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -1,14 +1,15 @@
%div
%h1
GitLab
- %span= Gitlab::VERSION
- %small= Gitlab::REVISION
- - if current_application_settings.version_check_enabled
+ Community Edition
+ - if user_signed_in?
+ %span= Gitlab::VERSION
+ %small= Gitlab::REVISION
= version_status_badge
%p.slead
GitLab is open source software to collaborate on code.
%br
- Manage git repositories with fine grained access controls that keep your code secure.
+ Manage git repositories with fine-grained access controls that keep your code secure.
%br
Perform code reviews and enhance collaboration with merge requests.
%br
@@ -17,6 +18,9 @@
Used by more than 100,000 organizations, GitLab is the most popular solution to manage git repositories on-premises.
%br
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
+ - if current_application_settings.help_page_text.present?
+ %hr
+ = markdown(current_application_settings.help_page_text)
%hr
@@ -24,29 +28,14 @@
.col-md-8
.documentation-index
= preserve do
- - readme_text = File.read(Rails.root.join("doc", "README.md"))
- - text = readme_text.dup
- - readme_text.scan(/\]\(([^(]+)\)/) { |match| text.gsub!(match.first, "help/#{match.first}") }
- = markdown text
-
+ = markdown(@help_index)
.col-md-4
.panel.panel-default
.panel-heading
Quick help
%ul.well-list
- %li
- See our website for
- = link_to 'getting help', promo_url + '/getting-help/'
- %li
- Use the
- = link_to 'search bar', '#', onclick: 'Shortcuts.focusSearch(event)'
- on the top of this page
- %li
- Use
- = link_to 'shortcuts', '#', onclick: 'Shortcuts.showHelp(event)'
- %li
- Get a support
- = link_to 'subscription', 'https://about.gitlab.com/pricing/'
- %li
- = link_to 'Compare', 'https://about.gitlab.com/features/#compare'
- GitLab editions
+ %li= link_to 'See our website for getting help', promo_url + '/getting-help/'
+ %li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)'
+ %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.showHelp(event)'
+ %li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
+ %li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
diff --git a/app/views/help/show.html.haml b/app/views/help/show.html.haml
index 8551496b98a..0398afb4c1d 100644
--- a/app/views/help/show.html.haml
+++ b/app/views/help/show.html.haml
@@ -1,3 +1,3 @@
- page_title @file.humanize, *@category.split("/").reverse.map(&:humanize)
.documentation.wiki
- = markdown @markdown.gsub('$your_email', current_user.email)
+ = markdown @markdown.gsub('$your_email', current_user.try(:email) || "email@example.com")
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
new file mode 100644
index 00000000000..e1bb88ca4ed
--- /dev/null
+++ b/app/views/import/fogbugz/new.html.haml
@@ -0,0 +1,25 @@
+- page_title "FogBugz Import"
+%h3.page-title
+ %i.fa.fa-bug
+ Import projects from FogBugz
+%hr
+
+= form_tag callback_import_fogbugz_path, class: 'form-horizontal' do
+ %p
+ To get started you enter your FogBugz URL and login information below.
+ In the next steps, you'll be able to map users and select the projects
+ you want to import.
+ .form-group
+ = label_tag :uri, 'FogBugz URL', class: 'control-label'
+ .col-sm-4
+ = text_field_tag :uri, nil, placeholder: 'https://mycompany.fogbugz.com', class: 'form-control'
+ .form-group
+ = label_tag :email, 'FogBugz Email', class: 'control-label'
+ .col-sm-4
+ = text_field_tag :email, nil, class: 'form-control'
+ .form-group
+ = label_tag :password, 'FogBugz Password', class: 'control-label'
+ .col-sm-4
+ = password_field_tag :password, nil, class: 'form-control'
+ .form-actions
+ = submit_tag 'Continue to the next step', class: 'btn btn-create'
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
new file mode 100644
index 00000000000..25cebfb3665
--- /dev/null
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -0,0 +1,49 @@
+- page_title 'User map', 'FogBugz import'
+%h3.page-title
+ %i.fa.fa-bug
+ Import projects from FogBugz
+%hr
+
+= form_tag create_user_map_import_fogbugz_path, class: 'form-horizontal' do
+ %p
+ Customize how FogBugz email addresses and usernames are imported into GitLab.
+ In the next step, you'll be able to select the projects you want to import.
+ %p
+ The user map is a mapping of the FogBugz users that participated on your projects to the way their email address and usernames wil be imported into GitLab. You can change this by populating the table below.
+ %ul
+ %li
+ %strong Default: Map a FogBugz account ID to a full name
+ %p
+ An empty GitLab User field will add the FogBugz user's full name
+ (e.g. "By John Smith") in the description of all issues and comments.
+ It will also associate and/or assign these issues and comments with
+ the project creator.
+ %li
+ %strong Map a FogBugz account ID to a GitLab user
+ %p
+ Selecting a GitLab user will add a link to the GitLab user in the descriptions
+ of issues and comments (e.g. "By <a href="#">@johnsmith</a>"). It will also
+ associate and/or assign these issues and comments with the selected user.
+
+ %table.table
+ %thead
+ %tr
+ %th ID
+ %th Name
+ %th Email
+ %th GitLab User
+ %tbody
+ - @user_map.each do |id, user|
+ %tr
+ %td= id
+ %td= text_field_tag "users[#{id}][name]", user[:name], class: 'form-control'
+ %td= text_field_tag "users[#{id}][email]", user[:email], class: 'form-control'
+ %td
+ = users_select_tag("users[#{id}][gitlab_user]", class: 'custom-form-control',
+ scope: :all, email_user: true, selected: user[:gitlab_user])
+
+ .form-actions
+ = submit_tag 'Continue to the next step', class: 'btn btn-create'
+
+:coffeescript
+ new UsersSelect()
diff --git a/app/views/import/fogbugz/status.html.haml b/app/views/import/fogbugz/status.html.haml
new file mode 100644
index 00000000000..f179ece402d
--- /dev/null
+++ b/app/views/import/fogbugz/status.html.haml
@@ -0,0 +1,51 @@
+- page_title "FogBugz import"
+%h3.page-title
+ %i.fa.fa-bug
+ Import projects from FogBugz
+
+- if @repos.any?
+ %p.light
+ Select projects you want to import.
+ %p.light
+ Optionally, you can
+ = link_to 'customize', new_user_map_import_fogbugz_path
+ how FogBugz email addresses and usernames are imported into GitLab.
+ %hr
+ %p
+ = button_tag 'Import all projects', class: 'btn btn-success js-import-all'
+
+%table.table.import-jobs
+ %thead
+ %tr
+ %th From FogBugz
+ %th To GitLab
+ %th Status
+ %tbody
+ - @already_added_projects.each do |project|
+ %tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
+ %td
+ = project.import_source
+ %td
+ %strong= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
+ %td.job-status
+ - if project.import_status == 'finished'
+ %span
+ %i.fa.fa-check
+ done
+ - elsif project.import_status == 'started'
+ %i.fa.fa-spinner.fa-spin
+ started
+ - else
+ = project.human_import_status_name
+
+ - @repos.each do |repo|
+ %tr{id: "repo_#{repo.id}"}
+ %td
+ = repo.name
+ %td.import-target
+ = "#{current_user.username}/#{repo.name}"
+ %td.import-actions.job-status
+ = button_tag "Import", class: "btn js-add-to-import"
+
+:coffeescript
+ new ImporterStatus("#{jobs_import_fogbugz_path}", "#{import_fogbugz_path}")
diff --git a/app/views/kaminari/gitlab/_first_page.html.haml b/app/views/kaminari/gitlab/_first_page.html.haml
index 41c9c0b3af6..ada7306d98d 100644
--- a/app/views/kaminari/gitlab/_first_page.html.haml
+++ b/app/views/kaminari/gitlab/_first_page.html.haml
@@ -5,5 +5,5 @@
-# num_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%span.first
+%li.first
= link_to_unless current_page.first?, raw(t 'views.pagination.first'), url, remote: remote
diff --git a/app/views/kaminari/gitlab/_last_page.html.haml b/app/views/kaminari/gitlab/_last_page.html.haml
index b03a206224c..3431d029bcc 100644
--- a/app/views/kaminari/gitlab/_last_page.html.haml
+++ b/app/views/kaminari/gitlab/_last_page.html.haml
@@ -5,5 +5,5 @@
-# num_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%span.last
+%li.last
= link_to_unless current_page.last?, raw(t 'views.pagination.last'), url, {remote: remote}
diff --git a/app/views/kaminari/gitlab/_paginator.html.haml b/app/views/kaminari/gitlab/_paginator.html.haml
index 4f7996e4996..2f645186921 100644
--- a/app/views/kaminari/gitlab/_paginator.html.haml
+++ b/app/views/kaminari/gitlab/_paginator.html.haml
@@ -7,11 +7,16 @@
-# paginator: the paginator that renders the pagination tags inside
= paginator.render do
%div.gl-pagination
- %ul.pagination
- = prev_page_tag unless current_page.first?
+ %ul.pagination.clearfix
+ - unless current_page.first?
+ = first_page_tag unless num_pages < 5 # As kaminari will always show the first 5 pages
+ = prev_page_tag
- 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?
+ - unless current_page.last?
+ = next_page_tag
+ = last_page_tag unless num_pages < 5
+
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 397649dacf8..74174a72f5a 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -3,6 +3,7 @@
%meta{charset: "utf-8"}
%meta{'http-equiv' => 'X-UA-Compatible', content: 'IE=edge'}
%meta{content: "GitLab Community Edition", name: "description"}
+ %meta{name: 'referrer', content: 'origin-when-cross-origin'}
%title= page_title
diff --git a/app/views/layouts/_init_auto_complete.html.haml b/app/views/layouts/_init_auto_complete.html.haml
index 3c58f10e759..035fe0056d3 100644
--- a/app/views/layouts/_init_auto_complete.html.haml
+++ b/app/views/layouts/_init_auto_complete.html.haml
@@ -1,3 +1,4 @@
+- project = @target_project || @project
:javascript
- GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(@project.namespace, @project, type: @noteable.class, type_id: params[:id])}"
+ GitLab.GfmAutoComplete.dataSource = "#{autocomplete_sources_namespace_project_path(project.namespace, project, type: @noteable.class, type_id: params[:id])}"
GitLab.GfmAutoComplete.setup();
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 0104d7198df..2468687b56d 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -6,20 +6,24 @@
= brand_header_logo
.gitlab-text-container
%h3 GitLab
+
- if defined?(sidebar) && sidebar
= render "layouts/nav/#{sidebar}"
- elsif current_user
= render 'layouts/nav/dashboard'
+ - else
+ = render 'layouts/nav/explore'
+
.collapse-nav
= render partial: 'layouts/collapse_button'
- if current_user
= link_to current_user, class: 'sidebar-user' do
- = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s32'
+ = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36'
.username
= current_user.username
.content-wrapper
- %div{ class: fluid_layout ? "container-fluid" : "container-fluid container-limited" }
+ = render "layouts/flash"
+ %div{ class: container_class }
.content
- = render "layouts/flash"
.clearfix
= yield
diff --git a/app/views/layouts/ci/_info.html.haml b/app/views/layouts/ci/_info.html.haml
new file mode 100644
index 00000000000..24c68a6dbf5
--- /dev/null
+++ b/app/views/layouts/ci/_info.html.haml
@@ -0,0 +1,2 @@
+- if current_user && current_user.is_admin? && Ci::Runner.count.zero?
+ = render 'ci/shared/no_runners'
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..af2545a22d8
--- /dev/null
+++ b/app/views/layouts/ci/_nav_admin.html.haml
@@ -0,0 +1,33 @@
+%ul.nav.nav-sidebar
+ = nav_link do
+ = link_to admin_root_path, title: 'Back to admin', data: {placement: 'right'}, class: 'back-link' do
+ = icon('caret-square-o-left fw')
+ %span
+ Back to admin
+
+ %li.separate-item
+ = nav_link path: 'projects#index' do
+ = link_to ci_admin_projects_path do
+ = icon('list-alt fw')
+ Projects
+ = nav_link path: 'events#index' do
+ = link_to ci_admin_events_path do
+ = icon('book fw')
+ Events
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to ci_admin_runners_path do
+ = icon('cog fw')
+ Runners
+ %small.pull-right
+ = Ci::Runner.count(:all)
+ = nav_link path: 'builds#index' do
+ = link_to ci_admin_builds_path do
+ = icon('link fw')
+ Builds
+ %small.pull-right
+ = Ci::Build.count(:all)
+ = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
+ = link_to ci_admin_application_settings_path do
+ = icon('cogs fw')
+ %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..545abc23d99
--- /dev/null
+++ b/app/views/layouts/ci/_nav_project.html.haml
@@ -0,0 +1,23 @@
+%ul.nav.nav-sidebar
+ = nav_link do
+ = link_to project_path(@project.gl_project), title: 'Back to project', data: {placement: 'right'}, class: 'back-link' do
+ = icon('caret-square-o-left fw')
+ %span
+ Back to project
+ %li.separate-item
+ = nav_link path: ['projects#show', 'commits#show', 'builds#show'] do
+ = link_to ci_project_path(@project) do
+ = icon('list-alt fw')
+ %span
+ Commits
+ %span.count= @project.commits.count
+ = nav_link path: ['services#index', 'services#edit'] do
+ = link_to ci_project_services_path(@project) do
+ = icon('share fw')
+ %span
+ Services
+ = nav_link path: 'events#index' do
+ = link_to ci_project_events_path(@project) do
+ = icon('book fw')
+ %span
+ Events
diff --git a/app/views/layouts/ci/_page.html.haml b/app/views/layouts/ci/_page.html.haml
new file mode 100644
index 00000000000..bb5ec727bff
--- /dev/null
+++ b/app/views/layouts/ci/_page.html.haml
@@ -0,0 +1,27 @@
+.page-with-sidebar{ class: nav_sidebar_class }
+ = render "layouts/broadcast"
+ .sidebar-wrapper.nicescroll
+ .header-logo
+ = link_to root_path, class: 'home', title: 'Dashboard', id: 'js-shortcuts-home', data: {toggle: 'tooltip', placement: 'bottom'} do
+ = brand_header_logo
+ .gitlab-text-container
+ %h3 GitLab
+
+ - if defined?(sidebar) && sidebar
+ = render "layouts/ci/#{sidebar}"
+ - elsif current_user
+ = render 'layouts/nav/dashboard'
+ .collapse-nav
+ = render partial: 'layouts/collapse_button'
+ - if current_user
+ = link_to current_user, class: 'sidebar-user' do
+ = image_tag avatar_icon(current_user.email, 60), alt: 'User activity', class: 'avatar avatar s36'
+ .username
+ = current_user.username
+ .content-wrapper
+ = render "layouts/flash"
+ = render 'layouts/ci/info'
+ %div{ class: container_class }
+ .content
+ .clearfix
+ = yield
diff --git a/app/views/layouts/ci/admin.html.haml b/app/views/layouts/ci/admin.html.haml
new file mode 100644
index 00000000000..c8cb185d28c
--- /dev/null
+++ b/app/views/layouts/ci/admin.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/head'
+ %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
+ - header_title = "Admin area"
+ - if current_user
+ = render "layouts/header/default", title: header_title
+ - else
+ = render "layouts/header/public", title: header_title
+
+ = render 'layouts/ci/page', sidebar: 'nav_admin'
diff --git a/app/views/layouts/ci/application.html.haml b/app/views/layouts/ci/application.html.haml
new file mode 100644
index 00000000000..38023468d0b
--- /dev/null
+++ b/app/views/layouts/ci/application.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/head'
+ %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
+ - header_title = "Continuous Integration"
+ - if current_user
+ = render "layouts/header/default", title: header_title
+ - else
+ = render "layouts/header/public", title: header_title
+
+ = render 'layouts/ci/page'
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..15478c3f5bc
--- /dev/null
+++ b/app/views/layouts/ci/project.html.haml
@@ -0,0 +1,11 @@
+!!! 5
+%html{ lang: "en"}
+ = render 'layouts/head'
+ %body{class: "ci-body #{user_application_theme}", 'data-page' => body_data_page}
+ - header_title @project.name, ci_project_path(@project)
+ - if current_user
+ = render "layouts/header/default", title: header_title
+ - else
+ = render "layouts/header/public", title: header_title
+
+ = render 'layouts/ci/page', sidebar: 'nav_project'
diff --git a/app/views/layouts/dashboard.html.haml b/app/views/layouts/dashboard.html.haml
index c72eca10bf4..cb96bcc2cf4 100644
--- a/app/views/layouts/dashboard.html.haml
+++ b/app/views/layouts/dashboard.html.haml
@@ -1,5 +1,5 @@
- page_title "Dashboard"
-- header_title "Dashboard", root_path
+- header_title "Dashboard", root_path unless header_title
- sidebar "dashboard"
= render template: "layouts/application"
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 1987bf1592a..95e077c339f 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -31,5 +31,5 @@
.container
.footer-links
= link_to "Explore", explore_root_path
- = link_to "Documentation", "http://doc.gitlab.com/"
+ = link_to "Help", help_path
= link_to "About GitLab", "https://about.gitlab.com/"
diff --git a/app/views/layouts/explore.html.haml b/app/views/layouts/explore.html.haml
index 17fee9c510d..df65792be73 100644
--- a/app/views/layouts/explore.html.haml
+++ b/app/views/layouts/explore.html.haml
@@ -1,8 +1,5 @@
- page_title "Explore"
-- if current_user
- - header_title "Dashboard", root_path
-- else
+- unless current_user
- header_title "Explore GitLab", explore_root_path
-- sidebar "dashboard"
= render template: "layouts/application"
diff --git a/app/views/layouts/group.html.haml b/app/views/layouts/group.html.haml
index db7dbf9bfe3..31888c5580e 100644
--- a/app/views/layouts/group.html.haml
+++ b/app/views/layouts/group.html.haml
@@ -1,5 +1,5 @@
- page_title @group.name
-- header_title @group.name, group_path(@group)
-- sidebar "group" unless sidebar
+- header_title group_title(@group) unless header_title
+- sidebar "group" unless sidebar
= render template: "layouts/application"
diff --git a/app/views/layouts/group_settings.html.haml b/app/views/layouts/group_settings.html.haml
index e303a561628..a1a1fc2f858 100644
--- a/app/views/layouts/group_settings.html.haml
+++ b/app/views/layouts/group_settings.html.haml
@@ -1,4 +1,5 @@
- page_title "Settings"
+- header_title group_title(@group, "Settings", edit_group_path(@group))
- sidebar "group_settings"
= render template: "layouts/group"
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 0b630b55c70..c31b1cbe9a8 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,5 +1,5 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %div{ class: fluid_layout ? "container-fluid" : "container-fluid container-limited" }
+ %div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
.header-content
%button.navbar-toggle{type: 'button'}
%span.sr-only Toggle navigation
@@ -7,8 +7,9 @@
.navbar-collapse.collapse
%ul.nav.navbar-nav.pull-right
- %li.hidden-sm.hidden-xs
- = render 'layouts/search'
+ - unless @disable_search_panel
+ %li.hidden-sm.hidden-xs
+ = render 'layouts/search'
%li.visible-sm.visible-xs
= link_to search_path, title: 'Search', data: {toggle: 'tooltip', placement: 'bottom'} do
= icon('search')
@@ -17,7 +18,7 @@
= link_to admin_root_path, title: 'Admin area', data: {toggle: 'tooltip', placement: 'bottom'} do
= icon('wrench fw')
- if current_user.can_create_project?
- %li.hidden-xs
+ %li
= link_to new_project_path, title: 'New project', data: {toggle: 'tooltip', placement: 'bottom'} do
= icon('plus fw')
%li
diff --git a/app/views/layouts/header/_public.html.haml b/app/views/layouts/header/_public.html.haml
index af4b9ba58f6..a6a26518a0e 100644
--- a/app/views/layouts/header/_public.html.haml
+++ b/app/views/layouts/header/_public.html.haml
@@ -1,9 +1,9 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %div{ class: fluid_layout ? "container-fluid" : "container-fluid container-limited" }
+ %div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
.header-content
- unless current_controller?('sessions')
.pull-right
- = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success btn-sm'
+ = link_to "Sign in", new_session_path(:user, redirect_to_referer: 'yes'), class: 'btn btn-sign-in btn-success'
%h1.title= title
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 2065be3828a..2079feeeab6 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -4,7 +4,7 @@
= icon('dashboard fw')
%span
Overview
- = nav_link(controller: :projects) do
+ = nav_link(controller: [:admin, :projects]) do
= link_to admin_namespaces_projects_path, title: 'Projects', data: {placement: 'right'} do
= icon('cube fw')
%span
@@ -24,6 +24,11 @@
= icon('key fw')
%span
Deploy Keys
+ = nav_link do
+ = link_to ci_admin_projects_path, title: 'Continuous Integration', data: {placement: 'right'} do
+ = icon('building fw')
+ %span
+ Continuous Integration
= nav_link(controller: :logs) do
= link_to admin_logs_path, title: 'Logs', data: {placement: 'right'} do
= icon('file-text fw')
@@ -57,6 +62,12 @@
%span
Service Templates
+ = nav_link(controller: :labels) do
+ = link_to admin_labels_path, title: 'Labels', data: {placement: 'right'} do
+ = icon('tags fw')
+ %span
+ Labels
+
= nav_link(controller: :abuse_reports) do
= link_to admin_abuse_reports_path, title: "Abuse reports" do
= icon('exclamation-circle fw')
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 0cf1c3d5d27..b1a1d531846 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,31 +1,30 @@
%ul.nav.nav-sidebar
- = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
- = link_to (current_user ? root_path : explore_root_path), title: 'Home', class: 'shortcuts-activity', data: {placement: 'right'} do
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: 'home'}) do
+ = link_to dashboard_projects_path, title: 'Projects', data: {placement: 'right'} do
= icon('home fw')
%span
Projects
= nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, title: 'Activity', data: {placement: 'right'} do
+ = link_to activity_dashboard_path, class: 'shortcuts-activity', title: 'Activity', data: {placement: 'right'} do
= icon('dashboard fw')
%span
Activity
= nav_link(controller: :groups) do
- = link_to (current_user ? dashboard_groups_path : explore_groups_path), title: 'Groups', data: {placement: 'right'} do
+ = link_to dashboard_groups_path, title: 'Groups', data: {placement: 'right'} do
= icon('group fw')
%span
Groups
- - if current_user
- = nav_link(controller: :milestones) do
- = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do
- = icon('clock-o fw')
- %span
- Milestones
- = nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do
- = icon('exclamation-circle fw')
- %span
- Issues
- %span.count= current_user.assigned_issues.opened.count
+ = nav_link(controller: :milestones) do
+ = link_to dashboard_milestones_path, title: 'Milestones', data: {placement: 'right'} do
+ = icon('clock-o fw')
+ %span
+ Milestones
+ = nav_link(path: 'dashboard#issues') do
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'shortcuts-issues', data: {placement: 'right'} do
+ = icon('exclamation-circle fw')
+ %span
+ Issues
+ %span.count= current_user.assigned_issues.opened.count
= nav_link(path: 'dashboard#merge_requests') do
= link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'shortcuts-merge_requests', data: {placement: 'right'} do
= icon('tasks fw')
@@ -33,18 +32,19 @@
Merge Requests
%span.count= current_user.assigned_merge_requests.opened.count
= nav_link(controller: :snippets) do
- = link_to (current_user ? user_snippets_path(current_user) : snippets_path), title: 'Your snippets', data: {placement: 'right'} do
+ = link_to dashboard_snippets_path, title: 'Your snippets', data: {placement: 'right'} do
= icon('clipboard fw')
%span
Snippets
- - if current_user
- = nav_link(controller: :profile) do
- = link_to profile_path, title: 'Profile settings', data: {toggle: 'tooltip', placement: 'bottom'} do
- = icon('user fw')
- %span
- Profile Settings
= nav_link(controller: :help) do
= link_to help_path, title: 'Help', data: {placement: 'right'} do
= icon('question-circle fw')
%span
Help
+
+ %li.separate-item
+ = nav_link(controller: :profile) do
+ = link_to profile_path, title: 'Profile settings', data: {placement: 'bottom'} do
+ = icon('user fw')
+ %span
+ Profile Settings
diff --git a/app/views/layouts/nav/_explore.html.haml b/app/views/layouts/nav/_explore.html.haml
new file mode 100644
index 00000000000..21e565972a7
--- /dev/null
+++ b/app/views/layouts/nav/_explore.html.haml
@@ -0,0 +1,21 @@
+%ul.nav.nav-sidebar
+ = nav_link(path: ['dashboard#show', 'root#show', 'projects#trending', 'projects#starred', 'projects#index'], html_options: {class: 'home'}) do
+ = link_to explore_root_path, title: 'Projects', data: {placement: 'right'} do
+ = icon('home fw')
+ %span
+ Projects
+ = nav_link(controller: :groups) do
+ = link_to explore_groups_path, title: 'Groups', data: {placement: 'right'} do
+ = icon('group fw')
+ %span
+ Groups
+ = nav_link(controller: :snippets) do
+ = link_to explore_snippets_path, title: 'Snippets', data: {placement: 'right'} do
+ = icon('clipboard fw')
+ %span
+ Snippets
+ = nav_link(controller: :help) do
+ = link_to help_path, title: 'Help', data: {placement: 'right'} do
+ = icon('question-circle fw')
+ %span
+ Help
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 695ce68a201..eb35af22b93 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -3,7 +3,7 @@
= link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to Dashboard
+ Back to dashboard
%li.separate-item
@@ -12,34 +12,35 @@
= icon('dashboard fw')
%span
Group
- - if current_user
- = nav_link(controller: [:group, :milestones]) do
- = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do
- = icon('clock-o fw')
+ - if can?(current_user, :read_group, @group)
+ - if current_user
+ = nav_link(controller: [:group, :milestones]) do
+ = link_to group_milestones_path(@group), title: 'Milestones', data: {placement: 'right'} do
+ = icon('clock-o fw')
+ %span
+ Milestones
+ = nav_link(path: 'groups#issues') do
+ = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do
+ = icon('exclamation-circle fw')
%span
- Milestones
- = nav_link(path: 'groups#issues') do
- = link_to issues_group_path(@group), title: 'Issues', data: {placement: 'right'} do
- = icon('exclamation-circle fw')
- %span
- Issues
- - if current_user
- %span.count= Issue.opened.of_group(@group).count
- = nav_link(path: 'groups#merge_requests') do
- = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do
- = icon('tasks fw')
- %span
- Merge Requests
- - if current_user
- %span.count= MergeRequest.opened.of_group(@group).count
- = nav_link(controller: [:group_members]) do
- = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do
- = icon('users fw')
- %span
- Members
- - if can?(current_user, :admin_group, @group)
- = nav_link(html_options: { class: "separate-item" }) do
- = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do
- = icon ('cogs fw')
+ Issues
+ - if current_user
+ %span.count= Issue.opened.of_group(@group).count
+ = nav_link(path: 'groups#merge_requests') do
+ = link_to merge_requests_group_path(@group), title: 'Merge Requests', data: {placement: 'right'} do
+ = icon('tasks fw')
+ %span
+ Merge Requests
+ - if current_user
+ %span.count= MergeRequest.opened.of_group(@group).count
+ = nav_link(controller: [:group_members]) do
+ = link_to group_group_members_path(@group), title: 'Members', data: {placement: 'right'} do
+ = icon('users fw')
%span
- Settings
+ Members
+ - if can?(current_user, :admin_group, @group)
+ = nav_link(html_options: { class: "separate-item" }) do
+ = link_to edit_group_path(@group), title: 'Settings', data: {placement: 'right'} do
+ = icon ('cogs fw')
+ %span
+ Settings
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index 33fd5fcef6c..5a47b8e6db2 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -3,7 +3,7 @@
= link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to Dashboard
+ Back to dashboard
%li.separate-item
@@ -11,7 +11,7 @@
= link_to profile_path, title: 'Profile', data: {placement: 'right'} do
= icon('user fw')
%span
- Profile
+ Profile Settings
= nav_link(controller: [:accounts, :two_factor_auths]) do
= link_to profile_account_path, title: 'Account', data: {placement: 'right'} do
= icon('gear fw')
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 5e7b902622b..a218ec7486c 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -4,13 +4,13 @@
= link_to group_path(@project.group), title: 'Back to group', data: {placement: 'right'}, class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to Group
+ Back to group
- else
= nav_link do
= link_to root_path, title: 'Back to dashboard', data: {placement: 'right'}, class: 'back-link' do
= icon('caret-square-o-left fw')
%span
- Back to Dashboard
+ Back to dashboard
%li.separate-item
@@ -26,28 +26,28 @@
Activity
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree)) do
- = link_to namespace_project_tree_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do
+ = link_to project_files_path(@project), title: 'Files', class: 'shortcuts-tree', data: {placement: 'right'} do
= icon('files-o fw')
%span
Files
- if project_nav_tab? :commits
= nav_link(controller: %w(commit commits compare repositories tags branches)) do
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do
+ = link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits', data: {placement: 'right'} do
= icon('history fw')
%span
Commits
- if project_nav_tab? :network
= nav_link(controller: %w(network)) do
- = link_to namespace_project_network_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do
+ = link_to namespace_project_network_path(@project.namespace, @project, current_ref), title: 'Network', class: 'shortcuts-network', data: {placement: 'right'} do
= icon('code-fork fw')
%span
Network
- if project_nav_tab? :graphs
= nav_link(controller: %w(graphs)) do
- = link_to namespace_project_graph_path(@project.namespace, @project, @ref || @repository.root_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do
+ = link_to namespace_project_graph_path(@project.namespace, @project, current_ref), title: 'Graphs', class: 'shortcuts-graphs', data: {placement: 'right'} do
= icon('area-chart fw')
%span
Graphs
@@ -76,6 +76,13 @@
Merge Requests
%span.count.merge_counter= @project.merge_requests.opened.count
+ - if @project.gitlab_ci?
+ = nav_link(controller: [:ci, :project]) do
+ = link_to ci_project_path(@project.gitlab_ci_project), title: 'Continuous Integration', data: {placement: 'right'} do
+ = icon('building fw')
+ %span
+ Continuous Integration
+
- if project_nav_tab? :settings
= nav_link(controller: [:project_members, :teams]) do
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab', data: {placement: 'right'} do
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 857fb199957..9279a846623 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -34,3 +34,29 @@
%span
Protected Branches
+ - if @project.gitlab_ci?
+ = nav_link(controller: :runners) do
+ = link_to namespace_project_runners_path(@project.namespace, @project), title: 'Runners', data: {placement: 'right'} do
+ = icon('cog fw')
+ %span
+ Runners
+ = nav_link(controller: :variables) do
+ = link_to namespace_project_variables_path(@project.namespace, @project) do
+ = icon('code fw')
+ %span
+ Variables
+ = nav_link path: 'triggers#index' do
+ = link_to namespace_project_triggers_path(@project.namespace, @project) do
+ = icon('retweet fw')
+ %span
+ Triggers
+ = nav_link path: 'ci_web_hooks#index' do
+ = link_to namespace_project_ci_web_hooks_path(@project.namespace, @project) do
+ = icon('link fw')
+ %span
+ CI Web Hooks
+ = nav_link path: 'ci_settings#edit' do
+ = link_to edit_namespace_project_ci_settings_path(@project.namespace, @project) do
+ = icon('building fw')
+ %span
+ CI Settings
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index ec209c38eed..2f7d7e86f56 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -42,5 +42,3 @@
- else
#{link_to "View it on GitLab", @target_url}
= email_action @target_url
- - if @project && !@disable_footer
- You're receiving this notification because you are a member of the #{link_to_unless @target_url, @project.name_with_namespace, namespace_project_url(@project.namespace, @project)} project team.
diff --git a/app/views/layouts/profile.html.haml b/app/views/layouts/profile.html.haml
index 77d2ccbf762..dfa6cc5702e 100644
--- a/app/views/layouts/profile.html.haml
+++ b/app/views/layouts/profile.html.haml
@@ -1,5 +1,5 @@
- page_title "Profile Settings"
-- header_title "Profile Settings", profile_path
+- header_title "Profile Settings", profile_path unless header_title
- sidebar "profile"
= render template: "layouts/application"
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 44afa33dfe5..abf73bcc709 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,12 +1,13 @@
- page_title @project.name_with_namespace
-- header_title project_title(@project)
-- sidebar "project" unless sidebar
+- header_title project_title(@project) unless header_title
+- sidebar "project" unless sidebar
- content_for :scripts_body_top do
+ - project = @target_project || @project
- if current_user
:javascript
- window.project_uploads_path = "#{namespace_project_uploads_path @project.namespace, @project}";
- window.markdown_preview_path = "#{markdown_preview_namespace_project_path(@project.namespace, @project)}";
+ window.project_uploads_path = "#{namespace_project_uploads_path project.namespace,project}";
+ window.markdown_preview_path = "#{markdown_preview_namespace_project_path(project.namespace, project)}";
- content_for :scripts_body do
= render "layouts/init_auto_complete" if current_user
diff --git a/app/views/layouts/project_settings.html.haml b/app/views/layouts/project_settings.html.haml
index 43401668334..59ce38f67bb 100644
--- a/app/views/layouts/project_settings.html.haml
+++ b/app/views/layouts/project_settings.html.haml
@@ -1,4 +1,5 @@
- page_title "Settings"
+- header_title project_title(@project, "Settings", edit_project_path(@project))
- sidebar "project_settings"
= render template: "layouts/project"
diff --git a/app/views/layouts/snippets.html.haml b/app/views/layouts/snippets.html.haml
index b77fe09fc2a..02ca3ee7a28 100644
--- a/app/views/layouts/snippets.html.haml
+++ b/app/views/layouts/snippets.html.haml
@@ -1,8 +1,3 @@
-- page_title 'Snippets'
-- if current_user
- - header_title "Dashboard", root_path
-- else
- - header_title 'Snippets', snippets_path
-- sidebar "dashboard"
+- header_title "Snippets", snippets_path
= render template: "layouts/application"
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 9db75bdb19e..34dbc60e19b 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -1,6 +1,6 @@
= "Merge Request ##{@merge_request.iid} was merged"
-Merge Request Url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
+Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
= merge_path_description(@merge_request, 'to')
diff --git a/app/views/notify/new_user_email.html.haml b/app/views/notify/new_user_email.html.haml
index 4feacdaacff..6b9b42dcf37 100644
--- a/app/views/notify/new_user_email.html.haml
+++ b/app/views/notify/new_user_email.html.haml
@@ -13,4 +13,4 @@
%p
= link_to "Click here to set your password", edit_password_url(@user, reset_password_token: @token)
%p
- = reset_token_expire_message
+ = raw reset_token_expire_message
diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml
index 3cd759f1f57..87b3ff7f0b3 100644
--- a/app/views/notify/project_was_moved_email.html.haml
+++ b/app/views/notify/project_was_moved_email.html.haml
@@ -1,5 +1,5 @@
%p
- Project was moved to another location
+ Project #{@old_path_with_namespace} was moved to another location
%p
The project is now located under
= link_to namespace_project_url(@project.namespace, @project) do
diff --git a/app/views/notify/project_was_moved_email.text.erb b/app/views/notify/project_was_moved_email.text.erb
index b3f18b35a4d..d8a23dabf49 100644
--- a/app/views/notify/project_was_moved_email.text.erb
+++ b/app/views/notify/project_was_moved_email.text.erb
@@ -1,4 +1,4 @@
-Project was moved to another location
+Project #{@old_path_with_namespace} was moved to another location
The project is now located under
<%= namespace_project_url(@project.namespace, @project) %>
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 767fe2e0e9a..cd7b1b0fe03 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,9 +1,7 @@
- page_title "Account"
-%h3.page-title
- = page_title
-%p.light
- Change your username and basic account settings.
-%hr
+- header_title page_title, profile_account_path
+- @blank_container = true
+
- if current_user.ldap_user?
.alert.alert-info
Some options are unavailable for LDAP accounts
@@ -69,7 +67,7 @@
- button_based_providers.each do |provider|
.btn-group
= link_to provider_image_tag(provider), user_omniauth_authorize_path(provider), method: :post, class: "btn btn-lg #{'active' if auth_active?(provider)}", "data-no-turbolink" => "true"
-
+
- if auth_active?(provider)
= link_to unlink_profile_account_path(provider: provider), method: :delete, class: 'btn btn-lg' do
= icon('close')
diff --git a/app/views/profiles/applications.html.haml b/app/views/profiles/applications.html.haml
index 3a3e6e1b1c4..2342936a5d5 100644
--- a/app/views/profiles/applications.html.haml
+++ b/app/views/profiles/applications.html.haml
@@ -1,13 +1,12 @@
- page_title "Applications"
-%h3.page-title
- = page_title
-%p.light
+- header_title page_title, applications_profile_path
+
+.gray-content-block.top-block
- if user_oauth_applications?
- Manage applications that can use GitLab as an OAuth provider,
+ Manage applications that can use GitLab as an OAuth provider,
and applications that you've authorized to use your account.
- else
Manage applications that you've authorized to use your account.
-%hr
- if user_oauth_applications?
.oauth-applications
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index 698d6037428..8fdba45b193 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,5 +1,8 @@
- page_title "Audit Log"
-%h3.page-title Audit Log
-%p.light History of authentications
+- header_title page_title, audit_log_profile_path
-= render 'event_table', events: @events \ No newline at end of file
+.gray-content-block.top-block
+ History of authentications
+
+.prepend-top-default
+= render 'event_table', events: @events
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index 66812872c41..1d140347a5f 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,12 +1,10 @@
- page_title "Emails"
-%h3.page-title
- = page_title
-%p.light
- Control emails linked to your account
-%hr
+- header_title page_title, profile_emails_path
+.gray-content-block.top-block
+ Control emails linked to your account
-%ul
+%ul.prepend-top-default
%li
Your
%b Primary Email
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 06655f7ba3a..14adba1c797 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,11 +1,12 @@
- page_title "SSH Keys"
-%h3.page-title
- = page_title
+- header_title page_title, profile_keys_path
+
+.gray-content-block.top-block
.pull-right
= link_to "Add SSH Key", new_profile_key_path, class: "btn btn-new"
-%p.light
- Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh", "README")
-%hr
+ .oneline
+ Before you can add an SSH key you need to
+ = link_to "generate it.", help_page_path("ssh", "README")
+.prepend-top-default
= render 'key_table'
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index db7fa2eabe3..8eebd96b674 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,10 +1,10 @@
- page_title "Notifications"
-%h3.page-title
- = page_title
-%p.light
+- header_title page_title, profile_notifications_path
+
+.gray-content-block.top-block
These are your global notification settings.
-%hr
+.prepend-top-default
= form_for @user, url: profile_notifications_path, method: :put, html: { class: 'update-notifications form-horizontal global-notifications-form' } do |f|
-if @user.errors.any?
%div.alert.alert-danger
@@ -33,7 +33,7 @@
= f.label :notification_level, value: Notification::N_MENTION do
= f.radio_button :notification_level, Notification::N_MENTION
.level-title
- Mention
+ On Mention
%p You will receive notifications only for comments in which you were @mentioned
.radio
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 399ae98adf9..fab7c45c9b2 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -1,13 +1,13 @@
- page_title "Password"
-%h3.page-title
- = page_title
-%p.light
+- header_title page_title, edit_profile_password_path
+
+.gray-content-block.top-block
- if @user.password_automatically_set?
Set your password.
- else
Change your password or recover your current one.
-%hr
-.update-password
+
+.update-password.prepend-top-default
= form_for @user, url: profile_password_path, method: :put, html: { class: 'form-horizontal' } do |f|
%div
%p.slead
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 9c6204963e0..d165f758c81 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -12,7 +12,7 @@
%ul
- @user.errors.full_messages.each do |msg|
%li= msg
-
+
- unless @user.password_automatically_set?
.form-group
= f.label :current_password, class: 'control-label'
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index aa0361a0a1b..01e285a8dfa 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,11 +1,11 @@
- page_title 'Preferences'
-%h3.page-title
- = page_title
-%p.light
+- header_title page_title, profile_preferences_path
+- @blank_container = true
+
+.alert.alert-help
These settings allow you to customize the appearance and behavior of the site.
They are saved with your account and will persist to any device you use to
access the site.
-%hr
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: {class: 'js-preferences-form form-horizontal'} do |f|
.panel.panel-default.application-theme
@@ -33,6 +33,13 @@
Behavior
.panel-body
.form-group
+ = f.label :layout, class: 'control-label' do
+ Layout width
+ .col-sm-10
+ = f.select :layout, layout_choices, {}, class: 'form-control'
+ .help-block
+ Choose between fixed (max. 1200px) and fluid (100%) application layout
+ .form-group
= f.label :dashboard, class: 'control-label' do
Default Dashboard
= link_to('(?)', help_page_path('profile', 'preferences') + '#default-dashboard', target: '_blank')
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 6c4b0ce757d..4433cab7782 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -2,6 +2,13 @@
$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
$('body').addClass('<%= user_application_theme %>')
+// Toggle container-fluid class
+if ('<%= current_user.layout %>' === 'fluid') {
+ $('.content-wrapper').find('.container-fluid').removeClass('container-limited')
+} else {
+ $('.content-wrapper').find('.container-fluid').addClass('container-limited')
+}
+
// Re-enable the "Save" button
$('input[type=submit]').enable()
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index c519e52e596..47412e2ef0c 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,14 +1,9 @@
-- page_title "Profile"
-%h3.page-title
- = page_title
-%p.light
+.gray-content-block.top-block
This information will appear on your profile.
- if current_user.ldap_user?
Some options are unavailable for LDAP accounts
-%hr
-
-
+.prepend-top-default
= form_for @user, url: profile_path, method: :put, html: { multipart: true, class: "edit_user form-horizontal" }, authenticity_token: true do |f|
-if @user.errors.any?
%div.alert.alert-danger
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index ee02b7f6a6c..1261f6254d7 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,5 +1,5 @@
= render 'projects/last_push'
-.hidden-xs
+.gray-content-block.activity-filter-block
- if current_user
%ul.nav.nav-pills.event_filter.pull-right
%li
@@ -7,7 +7,6 @@
%i.fa.fa-rss
= render 'shared/event_filter'
- %hr
.content_list{:"data-href" => activity_project_path(@project)}
= spinner
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index b93036e78e6..8c0980369fd 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -7,24 +7,28 @@
- if @project.description.present?
= markdown(@project.description, pipeline: :description)
-
- .project-repo-buttons
- = render 'projects/buttons/star'
-
- - unless empty_repo
- = render 'projects/buttons/fork'
-
- - if forked_from_project = @project.forked_from_project
- = link_to project_path(forked_from_project), class: 'btn' do
- = icon("code-fork fw")
- Forked from
+ - if forked_from_project = @project.forked_from_project
+ %p
+ Forked from
+ = link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
- - if can? current_user, :download_code, @project
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
- = icon('download fw')
- Download
- = render 'projects/buttons/dropdown'
- = render "shared/clone_panel"
+ .project-repo-buttons
+ .split-one
+ = render 'projects/buttons/star'
+
+ - unless empty_repo
+ = render 'projects/buttons/fork'
+
+ = render "shared/clone_panel"
+ .split-repo-buttons
+ - unless empty_repo
+ - if can? current_user, :download_code, @project
+ = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: @ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
+ = icon('download fw')
+
+ = render 'projects/buttons/dropdown'
+ = render 'projects/buttons/notifications'
+
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 30622d8a910..f0a3e416db7 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,14 +1,15 @@
- if event = last_push_event
- if show_last_push_widget?(event)
- .hidden-xs.center
- .slead
- %span You pushed to
- = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
- %strong= event.ref_name
- branch
- #{time_ago_with_tooltip(event.created_at)}
- %div
- = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
- Create Merge Request
- %hr
+ .gray-content-block.top-block.clear-block.hidden-xs
+ .event-last-push
+ .event-last-push-text
+ %span You pushed to
+ = link_to namespace_project_commits_path(event.project.namespace, event.project, event.ref_name) do
+ %strong= event.ref_name
+ branch
+ #{time_ago_with_tooltip(event.created_at)}
+
+ .pull-right
+ = link_to new_mr_path_from_push_event(event), title: "New Merge Request", class: "btn btn-info btn-sm" do
+ Create Merge Request
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index b7bca6dae09..507757f6a2b 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -1,6 +1,6 @@
.md-area
.md-header.clearfix
- %ul.nav.nav-tabs
+ %ul.center-top-menu
%li.active
= link_to '#md-write-holder', class: 'js-md-write-button', tabindex: '-1' do
Write
@@ -14,7 +14,7 @@
You are about to add
%strong
%span.js-referenced-users-count 0
- people
+ people
to the discussion. Proceed with caution.
%div
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index 5038edb95ed..5bc1999ec9d 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -5,7 +5,7 @@
&nbsp;
- if can?(current_user, :push_code, @project)
= link_to namespace_project_edit_blob_path(@project.namespace, @project, tree_join(@repository.root_ref, readme.name)), class: 'light' do
- %i.fa.fa-pencil
+ %i.fa-align.fa.fa-pencil
.wiki
= cache(readme_cache_key) do
= render_readme(readme)
diff --git a/app/views/projects/activity.html.haml b/app/views/projects/activity.html.haml
index 65674913bb0..555ed76426d 100644
--- a/app/views/projects/activity.html.haml
+++ b/app/views/projects/activity.html.haml
@@ -1 +1,4 @@
+- page_title "Activity"
+- header_title project_title(@project, "Activity", activity_project_path(@project))
+
= render 'projects/activity'
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index c1ec42aefca..6518c4173e1 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,4 +1,6 @@
- page_title "Blame", @blob.path, @ref
+- header_title project_title(@project, "Files", project_files_path(@project))
+
%h3.page-title Blame view
#tree-holder.tree-holder
diff --git a/app/views/projects/blob/_actions.html.haml b/app/views/projects/blob/_actions.html.haml
index 13f8271b979..373b3a0c5b0 100644
--- a/app/views/projects/blob/_actions.html.haml
+++ b/app/views/projects/blob/_actions.html.haml
@@ -17,6 +17,6 @@
tree_join(@commit.sha, @path)), class: 'btn btn-sm'
- if allowed_tree_edit?
- = button_tag class: 'remove-blob btn btn-sm btn-remove',
- 'data-toggle' => 'modal', 'data-target' => '#modal-remove-blob' do
- Remove
+ .btn-group{ role: "group" }
+ %button.btn.btn-default{ 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } Replace
+ %button.btn.btn-remove{ 'data-target' => '#modal-remove-blob', 'data-toggle' => 'modal' } Remove
diff --git a/app/views/projects/blob/_blob.html.haml b/app/views/projects/blob/_blob.html.haml
index 65c3ab10e02..b4c7d8b9b71 100644
--- a/app/views/projects/blob/_blob.html.haml
+++ b/app/views/projects/blob/_blob.html.haml
@@ -15,7 +15,7 @@
- else
= link_to title, '#'
-%ul.blob-commit-info.well.hidden-xs
+%ul.blob-commit-info.hidden-xs
- blob_commit = @repository.last_commit_for_path(@commit.id, blob.path)
= render blob_commit, project: @project
diff --git a/app/views/projects/blob/_header_title.html.haml b/app/views/projects/blob/_header_title.html.haml
new file mode 100644
index 00000000000..78c5ef20a5f
--- /dev/null
+++ b/app/views/projects/blob/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Files", project_files_path(@project))
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
new file mode 100644
index 00000000000..1a1df127703
--- /dev/null
+++ b/app/views/projects/blob/_upload.html.haml
@@ -0,0 +1,28 @@
+#modal-upload-blob.modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %h3.page-title #{title}
+ %p.light
+ From branch
+ %strong= @ref
+ .modal-body
+ = form_tag form_path, method: method, class: 'blob-file-upload-form-js form-horizontal' do
+ .dropzone
+ .dropzone-previews.blob-upload-dropzone-previews
+ %p.dz-message.light
+ Attach a file by drag &amp; drop or
+ = link_to 'click to upload', '#', class: "markdown-selector"
+ %br
+ .dropzone-alerts{class: "alert alert-danger data", style: "display:none"}
+ = render 'shared/commit_message_container', params: params,
+ placeholder: placeholder
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ = button_tag button_title, class: 'btn btn-small btn-primary btn-upload-file', id: 'submit-all'
+ = link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
+
+:coffeescript
+ disableButtonIfEmptyField $('.blob-file-upload-form-js').find('#commit_message'), '.btn-upload-file'
+ new BlobFileDropzone($('.blob-file-upload-form-js'), '#{method}')
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index a12cd660fc1..a811adc5094 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,6 +1,8 @@
- page_title "Edit", @blob.path, @ref
+= render "header_title"
+
.file-editor
- %ul.nav.nav-tabs.js-edit-mode
+ %ul.center-top-menu.no-bottom.js-edit-mode
%li.active
= link_to '#editor' do
%i.fa.fa-edit
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 7c2a4fece94..1950586b112 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,5 +1,14 @@
-- page_title "New File", @ref
-%h3.page-title New file
+- page_title "New File", @path.presence, @ref
+= render "header_title"
+
+.gray-content-block.top-block
+ Create a new file or
+ = link_to 'upload', '#modal-upload-blob',
+ { class: 'upload-link', 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal'}
+ an existing one
+
+= render 'projects/blob/upload', title: 'Upload', placeholder: 'Upload new file', button_title: 'Upload file', form_path: namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post
+
.file-editor
= form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal form-new-file js-requires-input') do
= render 'projects/blob/editor', ref: @ref
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index bd2fc43633c..fa4be4a1bc4 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -1,4 +1,5 @@
- page_title @blob.path, @ref
+= render "header_title"
= render 'projects/last_push'
@@ -10,3 +11,8 @@
- if allowed_tree_edit?
= render 'projects/blob/remove'
+
+ - title = "Replace #{@blob.name}"
+ = render 'projects/blob/upload', title: title, placeholder: title,
+ button_title: 'Replace file', form_path: namespace_project_update_blob_path(@project.namespace, @project, @id),
+ method: :put
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index a693c4b282f..cc0ec9483d2 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,20 +1,20 @@
- commit = @repository.commit(branch.target)
%li(class="js-branch-#{branch.name}")
- %h4
+ %div
= link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
%strong.str-truncated= branch.name
+ &nbsp;
- if branch.name == @repository.root_ref
- %span.label.label-info default
+ %span.label.label-primary default
- elsif @repository.merged_to_root_ref? branch.name
- %span.label.label-primary.has_tooltip(title="Merged into #{@repository.root_ref}")
- %i.fa.fa-check
+ %span.label.label-info.has_tooltip(title="Merged into #{@repository.root_ref}")
merged
- if @project.protected_branch? branch.name
%span.label.label-success
%i.fa.fa-lock
protected
- .pull-right
+ .controls.hidden-xs
- if create_mr_button?(@repository.root_ref, branch.name)
= link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do
= icon('plus')
@@ -30,8 +30,7 @@
= icon("trash-o")
- if commit
- %ul.list-unstyled
- = render 'projects/commits/inline_commit', commit: commit, project: @project
+ = render 'projects/branches/commit', commit: commit, project: @project
- else
%p
Cant find HEAD commit for this branch
diff --git a/app/views/projects/branches/_commit.html.haml b/app/views/projects/branches/_commit.html.haml
new file mode 100644
index 00000000000..68326e65d85
--- /dev/null
+++ b/app/views/projects/branches/_commit.html.haml
@@ -0,0 +1,7 @@
+.branch-commit.light
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
+ &middot;
+ %span.str-truncated
+ = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
+ &middot;
+ #{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 80acc937908..03ade02a0c8 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -1,7 +1,7 @@
- page_title "Branches"
+= render "projects/commits/header_title"
= render "projects/commits/head"
-%h3.page-title
- Branches
+.gray-content-block
.pull-right
- if can? current_user, :push_code, @project
= link_to new_namespace_project_branch_path(@project.namespace, @project), class: 'btn btn-create' do
@@ -24,9 +24,10 @@
= sort_title_recently_updated
= link_to namespace_project_branches_path(sort: 'last_updated') do
= sort_title_oldest_updated
-%hr
+ .oneline
+ Protected branches can be managed in project settings
- unless @branches.empty?
- %ul.bordered-list.top-list.all-branches
+ %ul.content-list.all-branches
- @branches.each do |branch|
= render "projects/branches/branch", branch: branch
= paginate @branches, theme: 'gitlab'
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 29e82b93883..f5577042ca4 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Branch"
+= render "projects/commits/header_title"
+
- if @error
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
diff --git a/app/views/projects/builds/_build.html.haml b/app/views/projects/builds/_build.html.haml
new file mode 100644
index 00000000000..21c543b38dd
--- /dev/null
+++ b/app/views/projects/builds/_build.html.haml
@@ -0,0 +1,50 @@
+- gl_project = build.project.gl_project
+%tr.build
+ %td.status
+ = ci_status_with_icon(build.status)
+
+ %td.build-link
+ = link_to namespace_project_build_path(gl_project.namespace, gl_project, build) do
+ %strong Build ##{build.id}
+
+ - if defined?(ref)
+ %td
+ = build.ref
+
+ %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 && can?(current_user, :manage_builds, gl_project)
+ .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.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.fa-repeat
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
new file mode 100644
index 00000000000..93cd4dcfd93
--- /dev/null
+++ b/app/views/projects/builds/show.html.haml
@@ -0,0 +1,159 @@
+.build-page
+ .gray-content-block
+ Build for commit
+ %strong.monospace
+ = link_to @build.commit.short_sha, ci_status_path(@build.commit)
+ from
+ %code #{@build.ref}
+
+ #up-build-trace
+ - if @commit.matrix_for_ref?(@build.ref)
+ %ul.center-top-menu.build-top-menu
+ - @commit.builds_without_retry_for_ref(@build.ref).each do |build|
+ %li{class: ('active' if build == @build) }
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ = ci_icon_for_status(build.status)
+ %span
+ - if build.name
+ = build.name
+ - else
+ = build.id
+
+
+ - unless @commit.builds_without_retry_for_ref(@build.ref).include?(@build)
+ %li.active
+ %a
+ Build ##{@build.id}
+ &middot;
+ %i.fa.fa-warning-sign
+ This build was retried.
+
+ .gray-content-block.second-block
+ .build-head
+ .clearfix
+ = ci_status_with_icon(@build.status)
+ - if @build.duration
+ %span
+ %i.fa.fa-time
+ #{duration_in_words(@build.finished_at, @build.started_at)}
+ .pull-right
+ = @build.updated_at.stamp('19:00 Aug 27')
+
+ .row.prepend-top-default
+ .col-md-9
+ .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.fa-angle-up
+ = link_to '#down-build-trace', class: 'btn' do
+ %i.fa.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 && can?(current_user, :manage_builds, @project)
+ .pull-right
+ - if @build.active?
+ = link_to "Cancel", cancel_ci_project_build_path(@ci_project, @build), class: 'btn btn-sm btn-danger'
+ - elsif @build.commands.present?
+ = link_to "Retry", retry_ci_project_build_path(@ci_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.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}
+ %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.table.builds
+ - @builds.each_with_index do |build, i|
+ %tr.build
+ %td
+ = ci_icon_for_status(build.status)
+ %td
+ = link_to namespace_project_build_path(@project.namespace, @project, @build) do
+ - if build.name
+ = build.name
+ - else
+ %span ##{build.id}
+
+ %td.status= build.status
+
+
+ = paginate @builds
+
+
+ :javascript
+ new CiBuild("#{namespace_project_build_path(@project.namespace, @project, @build)}", "#{@build.status}")
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index bc7625e8989..4580c912692 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,6 +1,6 @@
- if current_user
%span.dropdown
- %a.dropdown-toggle.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
+ %a.dropdown-new.btn.btn-new{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
%ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
- if can?(current_user, :create_issue, @project)
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 854c154824d..8f2f631eb7d 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -8,6 +8,5 @@
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn' do
= icon('code-fork fw')
- Fork
%span.count
= @project.forks_count
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
new file mode 100644
index 00000000000..4b69a6d7a6f
--- /dev/null
+++ b/app/views/projects/buttons/_notifications.html.haml
@@ -0,0 +1,14 @@
+- return unless @membership
+
+= form_tag profile_notifications_path, method: :put, remote: true, class: 'inline-form', id: 'notification-form' do
+ = hidden_field_tag :notification_type, 'project'
+ = hidden_field_tag :notification_id, @membership.id
+ = hidden_field_tag :notification_level
+ %span.dropdown
+ %a.dropdown-new.btn.btn-new#notifications-button{href: '#', "data-toggle" => "dropdown"}
+ = icon('bell')
+ = notification_label(@membership)
+ = icon('angle-down')
+ %ul.dropdown-menu.dropdown-menu-right.project-home-dropdown
+ - Notification.project_notification_levels.each do |level|
+ = notification_list_item(level, @membership)
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 5d7df5ae099..3501dddefbe 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,10 +1,6 @@
- if current_user
= link_to toggle_star_namespace_project_path(@project.namespace, @project), class: 'btn star-btn toggle-star', method: :post, remote: true do
= icon('star fw')
- - if current_user.starred?(@project)
- Unstar
- - else
- Star
%span.count
= @project.star_count
@@ -17,6 +13,5 @@
- else
= link_to new_user_session_path, class: 'btn has_tooltip star-btn', title: 'You must sign in to star a project' do
= icon('star fw')
- Star
%span.count
= @project.star_count
diff --git a/app/views/projects/ci_settings/_form.html.haml b/app/views/projects/ci_settings/_form.html.haml
new file mode 100644
index 00000000000..9f891f557a9
--- /dev/null
+++ b/app/views/projects/ci_settings/_form.html.haml
@@ -0,0 +1,103 @@
+%h3.page-title
+ CI settings
+%hr
+.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(@ci_project)}
+
+= nested_form_for @ci_project, url: namespace_project_ci_settings_path(@project.namespace, @project), html: { class: 'form-horizontal' } do |f|
+ - if @ci_project.errors.any?
+ #error_explanation
+ %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:"
+ .alert.alert-error
+ %ul
+ - @ci_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'
+ - unless @ci_project.new_record?
+ = link_to 'Remove Project', ci_project_path(@ci_project), method: :delete, data: { confirm: 'Project will be removed. Are you sure?' }, class: 'btn btn-danger pull-right'
diff --git a/app/views/projects/ci_settings/edit.html.haml b/app/views/projects/ci_settings/edit.html.haml
new file mode 100644
index 00000000000..e9040fe4337
--- /dev/null
+++ b/app/views/projects/ci_settings/edit.html.haml
@@ -0,0 +1,21 @@
+- if @ci_project.generated_yaml_config
+ %p.alert.alert-danger
+ CI Jobs are deprecated now, you can #{link_to "download", dumped_yaml_ci_project_path(@ci_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 @ci_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, @ci_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/projects/ci_web_hooks/index.html.haml b/app/views/projects/ci_web_hooks/index.html.haml
new file mode 100644
index 00000000000..6aebd7cfc4d
--- /dev/null
+++ b/app/views/projects/ci_web_hooks/index.html.haml
@@ -0,0 +1,92 @@
+%h3.page-title
+ CI Web hooks
+
+%p.light
+ Web Hooks can be used for binding events when build completed.
+
+%hr.clearfix
+
+= form_for @web_hook, url: namespace_project_ci_web_hooks_path(@project.namespace, @project), 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 @ci_project.commits.any?
+ = link_to 'Test Hook', test_namespace_project_ci_web_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped"
+ = link_to 'Remove', namespace_project_ci_web_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
+
+%h4 Web Hook data example
+
+:erb
+ <pre>
+ <code>
+ {
+ "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"
+ }
+ }
+ </code>
+ </pre>
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
new file mode 100644
index 00000000000..a634ae5dfda
--- /dev/null
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -0,0 +1,7 @@
+%ul.center-top-menu.commit-ci-menu
+ = nav_link(path: 'commit#show') do
+ = link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ Changes
+ = nav_link(path: 'commit#ci') do
+ = link_to ci_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ Builds
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 3f645b81397..fbf0a9ec0c3 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -38,10 +38,17 @@
- @commit.parents.each do |parent|
= link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent)
+- if @ci_commit
+ .pull-right
+ = link_to ci_status_path(@ci_commit), class: "ci-status ci-#{@ci_commit.status}" do
+ = ci_status_icon(@ci_commit)
+ build:
+ = @ci_commit.status
+
.commit-info-row.branches
%i.fa.fa-spinner.fa-spin
-.commit-box
+.commit-box.gray-content-block.middle-block
%h3.commit-title
= gfm escape_once(@commit.title)
- if @commit.description.present?
diff --git a/app/views/projects/commit/ci.html.haml b/app/views/projects/commit/ci.html.haml
new file mode 100644
index 00000000000..f4382e88046
--- /dev/null
+++ b/app/views/projects/commit/ci.html.haml
@@ -0,0 +1,62 @@
+- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+= render "projects/commits/header_title"
+= render "commit_box"
+= render "ci_menu"
+
+- if @ci_project && current_user && can?(current_user, :manage_builds, @project)
+ .pull-right
+ - if @ci_commit.builds.running_or_pending.any?
+ = link_to "Cancel", cancel_ci_project_commits_path(@ci_project, @ci_commit), class: 'btn btn-sm btn-danger'
+
+
+- if @ci_commit.yaml_errors.present?
+ .bs-callout.bs-callout-danger
+ %h4 Found errors in your .gitlab-ci.yml:
+ %ul
+ - @ci_commit.yaml_errors.split(",").each do |error|
+ %li= error
+
+- unless @ci_commit.ci_yaml_file
+ .bs-callout.bs-callout-warning
+ \.gitlab-ci.yml not found in this commit
+
+- @ci_commit.refs.each do |ref|
+ .gray-content-block.second-block
+ Builds for #{ref}
+ - if @ci_commit.duration_for_ref(ref) > 0
+ %small.pull-right
+ %i.fa.fa-time
+ #{time_interval_in_words @ci_commit.duration_for_ref(ref)}
+
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @ci_project && @ci_project.coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/builds/build", collection: @ci_commit.builds_without_retry.for_ref(ref), controls: true
+
+- if @ci_commit.retried_builds.any?
+ %h3
+ Retried builds
+
+ %table.table.builds
+ %thead
+ %tr
+ %th Status
+ %th Build ID
+ %th Ref
+ %th Stage
+ %th Name
+ %th Duration
+ %th Finished at
+ - if @ci_project && @ci_project.coverage_enabled?
+ %th Coverage
+ %th
+ = render partial: "projects/builds/build", collection: @ci_commit.retried_builds, ref: true
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 60b112e67d4..30a3973828f 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -1,4 +1,6 @@
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
+= render "projects/commits/header_title"
= render "commit_box"
+= render "ci_menu" if @ci_commit
= render "projects/diffs/diffs", diffs: @diffs, project: @project
= render "projects/notes/notes_with_form", view: params[:view]
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 74f8d8b15cf..cddd5aa3a83 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -4,7 +4,11 @@
- notes = commit.notes
- note_count = notes.user.count
-= cache [project.id, commit.id, note_count] do
+- ci_commit = project.ci_commit(commit.sha)
+- cache_key = [project.path_with_namespace, commit.id, note_count]
+- cache_key.push(ci_commit.status) if ci_commit
+
+= cache(cache_key) do
%li.commit.js-toggle-container
.commit-row-title
%strong.str-truncated
@@ -13,6 +17,10 @@
%a.text-expander.js-toggle-button ...
.pull-right
+ - if ci_commit
+ = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}" do
+ = ci_status_icon(ci_commit)
+ &nbsp;
= link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit_short_id"
.notes_count
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index e3d8cd0fdd5..a849bf84698 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,22 +1,18 @@
-%ul.nav.nav-tabs
+%ul.center-top-menu
= nav_link(controller: [:commit, :commits]) do
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do
- = icon("history")
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
Commits
%span.badge= number_with_delimiter(@repository.commit_count)
= nav_link(controller: :compare) do
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: @ref || @repository.root_ref) do
- = icon("exchange")
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
Compare
= nav_link(html_options: {class: branches_tab_class}) do
= link_to namespace_project_branches_path(@project.namespace, @project) do
- = icon("code-fork")
Branches
%span.badge.js-totalbranch-count= @repository.branches.size
= nav_link(controller: :tags) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
- = icon("tags")
Tags
%span.badge.js-totaltags-count= @repository.tags.length
diff --git a/app/views/projects/commits/_header_title.html.haml b/app/views/projects/commits/_header_title.html.haml
new file mode 100644
index 00000000000..e4385893dd9
--- /dev/null
+++ b/app/views/projects/commits/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Commits", project_commits_path(@project))
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 55054a31977..2dd99cc8215 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -1,26 +1,28 @@
- page_title "Commits", @ref
+= render "header_title"
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
= render "head"
-.tree-ref-holder
- = render 'shared/ref_switcher', destination: 'commits'
+.gray-content-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'commits'
-.commits-feed-holder.hidden-xs.hidden-sm
- - if create_mr_button?(@repository.root_ref, @ref)
- = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
- = icon('plus')
- Create Merge Request
+ .commits-feed-holder.hidden-xs.hidden-sm
+ - if create_mr_button?(@repository.root_ref, @ref)
+ = link_to create_mr_path(@repository.root_ref, @ref), class: 'btn btn-success' do
+ = icon('plus')
+ Create Merge Request
- - if current_user && current_user.private_token
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'prepend-left-10 btn' do
- = icon("rss")
+ - if current_user && current_user.private_token
+ = link_to namespace_project_commits_path(@project.namespace, @project, @ref, {format: :atom, private_token: current_user.private_token}), title: "Commits Feed", class: 'prepend-left-10 btn' do
+ = icon("rss")
-%ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
%div{id: dom_id(@project)}
#commits-list= render "commits", project: @project
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index 3019893d12c..efc25eda26b 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,5 +1,5 @@
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
- .clearfix.append-bottom-20
+ .clearfix
- if params[:to] && params[:from]
= link_to 'switch', {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has_tooltip', title: 'Switch base of comparison'}
.form-group
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index d1e579a2ede..02be5a2d07f 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -1,9 +1,8 @@
- page_title "Compare"
+= render "projects/commits/header_title"
= render "projects/commits/head"
-%h3.page-title
- Compare View
-%p.slead
+.gray-content-block
Compare branches, tags or commit ranges.
%br
Fill input field with commit id like
@@ -14,4 +13,5 @@
%br
Changes are shown <b>from</b> the version in the first field <b>to</b> the version in the second field.
-= render "form"
+.prepend-top-20
+ = render "form"
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index 3670dd5c13b..39755efd2fd 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,16 +1,17 @@
- page_title "#{params[:from]}...#{params[:to]}"
+= render "projects/commits/header_title"
= render "projects/commits/head"
-%h3.page-title
- Compare View
-= render "form"
+.gray-content-block
+ = render "form"
- if @commits.present?
- = render "projects/commits/commit_list"
- = render "projects/diffs/diffs", diffs: @diffs, project: @project
+ .prepend-top-20
+ = render "projects/commits/commit_list"
+ = render "projects/diffs/diffs", diffs: @diffs, project: @project
- else
- .light-well
+ .light-well.prepend-top-20
.center
%h4
There isn't anything to compare.
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 30943f49bba..4f1965bfb39 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,21 +1,26 @@
- if params[:view] == 'parallel'
- fluid_layout true
-.prepend-top-20.append-bottom-20
- .pull-right
+- diff_files = safe_diff_files(diffs)
+
+.gray-content-block.second-block
+ .inline-parallel-buttons
.btn-group
= inline_diff_btn
= parallel_diff_btn
- = render 'projects/diffs/stats', diffs: diffs
-
-- diff_files = safe_diff_files(diffs)
+ = render 'projects/diffs/stats', diff_files: diff_files
- if diff_files.count < diffs.size
= render 'projects/diffs/warning', diffs: diffs, shown_files_count: diff_files.count
.files
- diff_files.each_with_index do |diff_file, index|
- = render 'projects/diffs/file', diff_file: diff_file, i: index, project: project
+ - diff_commit = commit_for_diff(diff_file.diff)
+ - blob = project.repository.blob_for_diff(diff_commit, diff_file.diff)
+ - next unless blob
+
+ = render 'projects/diffs/file', i: index, project: project,
+ diff_file: diff_file, diff_commit: diff_commit, blob: blob
- if @diff_timeout
.alert.alert-danger
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 99ee23a1ddc..4617b188150 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -1,24 +1,18 @@
-- blob = project.repository.blob_for_diff(@commit, diff_file.diff)
-- return unless blob
-- blob_diff_path = namespace_project_blob_diff_path(project.namespace, project, tree_join(@commit.id, diff_file.file_path))
-.diff-file{id: "diff-#{i}", data: {blob_diff_path: blob_diff_path }}
+.diff-file{id: "diff-#{i}", data: diff_file_html_data(project, diff_commit, diff_file)}
.diff-header{id: "file-path-#{hexdigest(diff_file.new_path || diff_file.old_path)}"}
- - if diff_file.deleted_file
- %span="#{diff_file.old_path} deleted"
-
- .diff-btn-group
- - if @commit.parent_ids.present?
- = view_file_btn(@commit.parent_id, diff_file, project)
- - elsif diff_file.diff.submodule?
+ - if diff_file.diff.submodule?
%span
- submodule_item = project.repository.blob_at(@commit.id, diff_file.file_path)
= submodule_link(submodule_item, @commit.id, project.repository)
- else
%span
- - if diff_file.renamed_file
+ - if diff_file.deleted_file
+ = "#{diff_file.old_path} deleted"
+ - elsif diff_file.renamed_file
= "#{diff_file.old_path} renamed to #{diff_file.new_path}"
- else
= diff_file.new_path
+
- if diff_file.mode_changed?
%span.file-mode= "#{diff_file.diff.a_mode} → #{diff_file.diff.b_mode}"
@@ -28,12 +22,12 @@
%i.fa.fa-comments
&nbsp;
- - if @merge_request && @merge_request.source_project
+ - if editable_diff?(diff_file)
= edit_blob_link(@merge_request.source_project,
@merge_request.source_branch, diff_file.new_path,
after: '&nbsp;', from_merge_request_id: @merge_request.id)
- = view_file_btn(@commit.id, diff_file, project)
+ = view_file_btn(diff_commit.id, diff_file, project)
.diff-content.diff-wrap-lines
-# Skipp all non non-supported blobs
diff --git a/app/views/projects/diffs/_stats.html.haml b/app/views/projects/diffs/_stats.html.haml
index 1625930615a..ea2a3e01277 100644
--- a/app/views/projects/diffs/_stats.html.haml
+++ b/app/views/projects/diffs/_stats.html.haml
@@ -2,37 +2,35 @@
.commit-stat-summary
Showing
= link_to '#', class: 'js-toggle-button' do
- %strong #{pluralize(diffs.count, "changed file")}
- - if current_controller?(:commit)
- - unless @commit.has_zero_stats?
- with
- %strong.cgreen #{@commit.stats.additions} additions
- and
- %strong.cred #{@commit.stats.deletions} deletions
+ %strong #{pluralize(diff_files.count, "changed file")}
+ with
+ %strong.cgreen #{diff_files.sum(&:added_lines)} additions
+ and
+ %strong.cred #{diff_files.sum(&:removed_lines)} deletions
.file-stats.js-toggle-content.hide
- %ul.bordered-list
- - diffs.each_with_index do |diff, i|
+ %ul
+ - diff_files.each_with_index do |diff_file, i|
%li
- - if diff.deleted_file
+ - if diff_file.deleted_file
%span.deleted-file
%a{href: "#diff-#{i}"}
%i.fa.fa-minus
- = diff.old_path
- - elsif diff.renamed_file
+ = diff_file.old_path
+ - elsif diff_file.renamed_file
%span.renamed-file
%a{href: "#diff-#{i}"}
%i.fa.fa-minus
- = diff.old_path
+ = diff_file.old_path
&rarr;
- = diff.new_path
- - elsif diff.new_file
+ = diff_file.new_path
+ - elsif diff_file.new_file
%span.new-file
%a{href: "#diff-#{i}"}
%i.fa.fa-plus
- = diff.new_path
+ = diff_file.new_path
- else
%span.edit-file
%a{href: "#diff-#{i}"}
%i.fa.fa-adjust
- = diff.new_path
+ = diff_file.new_path
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index e8e65d87f47..90dce739992 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -1,10 +1,11 @@
+- @blank_container = true
+
.project-edit-container
.project-edit-errors
.project-edit-content
- %div
- %h3.page-title
+ .panel.panel-default
+ .panel-heading
Project settings
- %hr
.panel-body
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "edit_project form-horizontal fieldset-form" }, authenticity_token: true do |f|
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index e577d35d560..e06454fd148 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -1,10 +1,11 @@
-- if current_user && can?(current_user, :download_code, @project)
- = render 'shared/no_ssh'
- = render 'shared/no_password'
-
+.alert_holder
+ - if current_user && can?(current_user, :download_code, @project)
+ = render 'shared/no_ssh'
+ = render 'shared/no_password'
+
= render "home_panel"
-.center.light-well
+.gray-content-block.center
%h3.page-title
The repository for this project is empty
%p
@@ -15,38 +16,39 @@
file to this project.
.prepend-top-20
-%h3.page-title
- Command line instructions
-%div.git-empty
- %fieldset
- %h5 Git global setup
- %pre.light-well
- :preserve
- git config --global user.name "#{git_user_name}"
- git config --global user.email "#{git_user_email}"
+.empty_wrapper
+ %h3.page-title-empty
+ Command line instructions
+ %div.git-empty
+ %fieldset
+ %h5 Git global setup
+ %pre.light-well
+ :preserve
+ git config --global user.name "#{h git_user_name}"
+ git config --global user.email "#{h git_user_email}"
- %fieldset
- %h5 Create a new repository
- %pre.light-well
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- cd #{@project.path}
- touch README.md
- git add README.md
- git commit -m "add README"
- git push -u origin master
+ %fieldset
+ %h5 Create a new repository
+ %pre.light-well
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ cd #{h @project.path}
+ touch README.md
+ git add README.md
+ git commit -m "add README"
+ git push -u origin master
- %fieldset
- %h5 Existing folder or Git repository
- %pre.light-well
- :preserve
- cd existing_folder
- git init
- git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
- git add .
- git commit
- git push -u origin master
+ %fieldset
+ %h5 Existing folder or Git repository
+ %pre.light-well
+ :preserve
+ cd existing_folder
+ git init
+ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
+ git add .
+ git commit
+ git push -u origin master
-- if can? current_user, :remove_project, @project
- .prepend-top-20
- = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
+ - if can? current_user, :remove_project, @project
+ .prepend-top-20
+ = link_to 'Remove project', [@project.namespace.becomes(Namespace), @project], data: { confirm: remove_project_message(@project)}, method: :delete, class: "btn btn-remove pull-right"
diff --git a/app/views/projects/forks/new.html.haml b/app/views/projects/forks/new.html.haml
index b7a2ed68e25..cd5f3a5d39e 100644
--- a/app/views/projects/forks/new.html.haml
+++ b/app/views/projects/forks/new.html.haml
@@ -10,21 +10,22 @@
- group.each do |namespace|
.col-md-2.col-sm-3
- if fork = namespace.find_fork_of(@project)
- .thumbnail.fork-exists-thumbnail
+ .fork-thumbnail
= link_to project_path(fork), title: "Visit project fork", class: 'has_tooltip' do
- = image_tag namespace_icon(namespace, 200)
+ = image_tag namespace_icon(namespace, 100)
.caption
- %h4=namespace.human_name
- %p
- = namespace.path
+ %strong
+ = namespace.human_name
+ %div.text-primary
+ Already forked
+
- else
- .thumbnail.fork-thumbnail
+ .fork-thumbnail
= link_to namespace_project_fork_path(@project.namespace, @project, namespace_key: namespace.id), title: "Fork here", method: "POST", class: 'has_tooltip' do
- = image_tag namespace_icon(namespace, 200)
+ = image_tag namespace_icon(namespace, 100)
.caption
- %h4=namespace.human_name
- %p
- = namespace.path
+ %strong
+ = namespace.human_name
%p.light
Fork is a copy of a project repository.
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 9383df13305..bbfaf422a82 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -3,3 +3,7 @@
= link_to 'Contributors', namespace_project_graph_path
= nav_link(action: :commits) do
= link_to 'Commits', commits_namespace_project_graph_path
+ - if @project.gitlab_ci?
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/graphs/_header_title.html.haml b/app/views/projects/graphs/_header_title.html.haml
new file mode 100644
index 00000000000..1e2f61cd22b
--- /dev/null
+++ b/app/views/projects/graphs/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Graphs", namespace_project_graph_path(@project.namespace, @project, current_ref))
diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml
new file mode 100644
index 00000000000..4f69cc64f7c
--- /dev/null
+++ b/app/views/projects/graphs/ci.html.haml
@@ -0,0 +1,7 @@
+- page_title "Continuous Integration", "Graphs"
+= render "header_title"
+= render 'head'
+#charts.ci-charts
+ = render 'projects/graphs/ci/builds'
+ = render 'projects/graphs/ci/build_times'
+= render 'projects/graphs/ci/overall'
diff --git a/app/views/projects/graphs/ci/_build_times.haml b/app/views/projects/graphs/ci/_build_times.haml
new file mode 100644
index 00000000000..c3c2f572414
--- /dev/null
+++ b/app/views/projects/graphs/ci/_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/projects/graphs/ci/_builds.haml b/app/views/projects/graphs/ci/_builds.haml
new file mode 100644
index 00000000000..1b0039fb834
--- /dev/null
+++ b/app/views/projects/graphs/ci/_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/projects/graphs/ci/_overall.haml b/app/views/projects/graphs/ci/_overall.haml
new file mode 100644
index 00000000000..9550d719471
--- /dev/null
+++ b/app/views/projects/graphs/ci/_overall.haml
@@ -0,0 +1,22 @@
+- ci_project = @project.gitlab_ci_project
+%fieldset
+ %legend Overall
+ %p
+ Total:
+ %strong= pluralize ci_project.builds.count(:all), 'build'
+ %p
+ Successful:
+ %strong= pluralize ci_project.builds.success.count(:all), 'build'
+ %p
+ Failed:
+ %strong= pluralize ci_project.builds.failed.count(:all), 'build'
+
+ %p
+ Success ratio:
+ %strong
+ #{success_ratio(ci_project.builds.success, ci_project.builds.failed)}%
+
+ %p
+ Commits covered:
+ %strong
+ = ci_project.commits.count(:all)
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index a357736bf52..112be875b6b 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -1,4 +1,5 @@
-- page_title "Commit statistics"
+- page_title "Commits", "Graphs"
+= render "header_title"
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'graphs_commits'
= render 'head'
@@ -31,61 +32,55 @@
%div
%p.slead
Commits per day of month
- %canvas#month-chart{width: 800, height: 400}
+ %canvas#month-chart
.row
.col-md-6
%div
%p.slead
Commits per day hour (UTC)
- %canvas#hour-chart{width: 800, height: 400}
+ %canvas#hour-chart
.col-md-6
%div
%p.slead
Commits per weekday
- %canvas#weekday-chart{width: 800, height: 400}
+ %canvas#weekday-chart
:coffeescript
- data = {
- labels : #{@commits_per_time.keys.to_json},
- datasets : [{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data : #{@commits_per_time.values.to_json}
- }]
- }
+ responsiveChart = (selector, data) ->
+ options = { "scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2, maintainAspectRatio: false }
- ctx = $("#hour-chart").get(0).getContext("2d");
- new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
+ # get selector by context
+ ctx = selector.get(0).getContext("2d")
+ # pointing parent container to make chart.js inherit its width
+ container = $(selector).parent()
- data = {
- labels : #{@commits_per_week_days.keys.to_json},
- datasets : [{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data : #{@commits_per_week_days.values.to_json}
- }]
- }
+ generateChart = ->
+ selector.attr('width', $(container).width())
+ new Chart(ctx).Bar(data, options)
+
+ # enabling auto-resizing
+ $(window).resize( generateChart )
- ctx = $("#weekday-chart").get(0).getContext("2d");
- new Chart(ctx).Bar(data,{"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
+ generateChart()
- data = {
- labels : #{@commits_per_month.keys.to_json},
- datasets : [{
- fillColor : "rgba(220,220,220,0.5)",
- strokeColor : "rgba(220,220,220,1)",
- barStrokeWidth: 1,
- barValueSpacing: 1,
- barDatasetSpacing: 1,
- data : #{@commits_per_month.values.to_json}
- }]
+ chartData = (keys, values) ->
+ data = {
+ labels : keys,
+ datasets : [{
+ fillColor : "rgba(220,220,220,0.5)",
+ strokeColor : "rgba(220,220,220,1)",
+ barStrokeWidth: 1,
+ barValueSpacing: 1,
+ barDatasetSpacing: 1,
+ data : values
+ }]
}
- ctx = $("#month-chart").get(0).getContext("2d");
- new Chart(ctx).Bar(data, {"scaleOverlay": true, responsive: true, pointHitDetectionRadius: 2})
+ hourData = chartData(#{@commits_per_time.keys.to_json}, #{@commits_per_time.values.to_json})
+ responsiveChart($('#hour-chart'), hourData)
+
+ dayData = chartData(#{@commits_per_week_days.keys.to_json}, #{@commits_per_week_days.values.to_json})
+ responsiveChart($('#weekday-chart'), dayData)
+
+ monthData = chartData(#{@commits_per_month.keys.to_json}, #{@commits_per_month.values.to_json})
+ responsiveChart($('#month-chart'), monthData)
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index ecdd0eaf52f..bd342911e49 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,4 +1,5 @@
-- page_title "Contributor statistics"
+- page_title "Contributors", "Graphs"
+= render "header_title"
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'graphs'
= render 'head'
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 39fe0fc1c4f..06886d215a3 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -3,8 +3,12 @@
.center
%h2
%i.fa.fa-spinner.fa-spin
- Import in progress.
- %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)}
+ - if @project.forked?
+ Forking in progress.
+ - else
+ Import in progress.
+ - unless @project.forked?
+ %p.monospace git clone --bare #{hidden_pass_url(@project.import_url)}
%p Please wait while we import the repository for you. Refresh at will.
:javascript
new ProjectImport();
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index f61ae957208..d4a98eca473 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -7,21 +7,24 @@
= render 'shared/show_aside'
+.gray-content-block.second-block
+ .row
+ .col-md-9
+ .votes-holder.pull-right
+ #votes= render 'votes/votes_block', votable: @issue
+ .participants
+ %span= pluralize(@participants.count, 'participant')
+ - @participants.each do |participant|
+ = link_to_member(@project, participant, name: false, size: 24)
+ .col-md-3
+ %span.slead.has_tooltip{title: 'Cross-project reference'}
+ = cross_project_reference(@project, @issue)
+
.row
%section.col-md-9
- .votes-holder.pull-right
- #votes= render 'votes/votes_block', votable: @issue
- .participants
- %span= pluralize(@participants.count, 'participant')
- - @participants.each do |participant|
- = link_to_member(@project, participant, name: false, size: 24)
.voting_notes#notes= render 'projects/notes/notes_with_form'
%aside.col-md-3
.issuable-affix
- .clearfix
- %span.slead.has_tooltip{title: 'Cross-project reference'}
- = cross_project_reference(@project, @issue)
- %hr
.context
= render 'shared/issuable/context', issuable: @issue
diff --git a/app/views/projects/issues/_header_title.html.haml b/app/views/projects/issues/_header_title.html.haml
new file mode 100644
index 00000000000..99f03549c44
--- /dev/null
+++ b/app/views/projects/issues/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Issues", namespace_project_issues_path(@project.namespace, @project))
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index b6910c8f796..55ce912829d 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -41,4 +41,4 @@
= issue.task_status
.pull-right.issue-updated-at
- %small updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
+ %span updated #{time_ago_with_tooltip(issue.updated_at, placement: 'bottom', html_class: 'issue_update_ago')}
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index 5d243adb5fe..a3399c57aa2 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,9 +1,8 @@
-.panel.panel-default
- %ul.well-list.issues-list
- = render @issues
- - if @issues.blank?
- %li
- .nothing-here-block No issues to show
+%ul.content-list.issues-list
+ = render @issues
+ - if @issues.blank?
+ %li
+ .nothing-here-block No issues to show
- if @issues.present?
.pull-right
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index d06225f5488..d6260ab2900 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,10 +1,12 @@
- page_title "Issues"
+= render "header_title"
+
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_issues_url(@project.namespace, @project, :atom, private_token: current_user.private_token), title: "#{@project.name} issues")
-.append-bottom-10
- .pull-right
+.project-issuable-filter
+ .controls
.pull-left
- if current_user
.hidden-xs.pull-left
diff --git a/app/views/projects/issues/new.html.haml b/app/views/projects/issues/new.html.haml
index da6edd5c2d2..153447baa1b 100644
--- a/app/views/projects/issues/new.html.haml
+++ b/app/views/projects/issues/new.html.haml
@@ -1,2 +1,4 @@
- page_title "New Issue"
+= render "header_title"
+
= render "form"
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index e7b14e7582c..5cb814c9ea8 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,15 +1,18 @@
- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+= render "header_title"
+
.issue
.issue-details.issuable-details
- %h4.page-title
+ .page-title
.issue-box{ class: issue_box_class(@issue) }
- if @issue.closed?
Closed
- else
Open
- Issue ##{@issue.iid}
- %small.creator
- &middot; created by #{link_to_member(@project, @issue.author)}
+ %span.issue-id Issue ##{@issue.iid}
+ %span.creator
+ &middot; created by #{link_to_member(@project, @issue.author, size: 24)}
+ &middot;
= time_ago_with_tooltip(@issue.created_at, placement: 'bottom', html_class: 'issue_created_ago')
- if @issue.updated_at != @issue.created_at
%span
@@ -32,18 +35,17 @@
= icon('pencil-square-o')
Edit
- %hr
- %h2.issue-title
- = gfm escape_once(@issue.title)
- %div
- - if @issue.description.present?
- .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''}
- .wiki
- = preserve do
- = markdown(@issue.description)
- %textarea.hidden.js-task-list-field
- = @issue.description
+ .gray-content-block.middle-block
+ %h2.issue-title
+ = gfm escape_once(@issue.title)
+ %div
+ - if @issue.description.present?
+ .description{class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : ''}
+ .wiki
+ = preserve do
+ = markdown(@issue.description)
+ %textarea.hidden.js-task-list-field
+ = @issue.description
- %hr
.issue-discussion
= render 'projects/issues/discussion'
diff --git a/app/views/projects/labels/_header_title.html.haml b/app/views/projects/labels/_header_title.html.haml
new file mode 100644
index 00000000000..abe28da483b
--- /dev/null
+++ b/app/views/projects/labels/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Labels", namespace_project_labels_path(@project.namespace, @project))
diff --git a/app/views/projects/labels/edit.html.haml b/app/views/projects/labels/edit.html.haml
index 645402667fd..bc4ab0ca27c 100644
--- a/app/views/projects/labels/edit.html.haml
+++ b/app/views/projects/labels/edit.html.haml
@@ -1,4 +1,6 @@
- page_title "Edit", @label.name, "Labels"
+= render "header_title"
+
%h3
Edit label
%span.light #{@label.name}
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index d44fe486212..97175f8232b 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -1,14 +1,16 @@
- page_title "Labels"
-- if can? current_user, :admin_label, @project
- = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
- New label
-%h3.page-title
- Labels
-%hr
+= render "header_title"
+
+.gray-content-block.top-block
+ - if can? current_user, :admin_label, @project
+ = link_to new_namespace_project_label_path(@project.namespace, @project), class: "pull-right btn btn-new" do
+ New label
+ .oneline
+ Labels can be applied to issues and merge requests.
.labels
- if @labels.present?
- %ul.bordered-list.manage-labels-list
+ %ul.content-list.manage-labels-list
= render @labels
= paginate @labels, theme: 'gitlab'
- else
diff --git a/app/views/projects/labels/new.html.haml b/app/views/projects/labels/new.html.haml
index b3ef17025c3..342ad4f3f95 100644
--- a/app/views/projects/labels/new.html.haml
+++ b/app/views/projects/labels/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Label"
+= render "header_title"
+
%h3 New label
.back-link
= link_to namespace_project_labels_path(@project.namespace, @project) do
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index f855dfec321..38e66c3828b 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -7,18 +7,21 @@
= render 'shared/show_aside'
+.gray-content-block.second-block
+ .row
+ .col-md-9
+ .votes-holder.pull-right
+ #votes= render 'votes/votes_block', votable: @merge_request
+ = render "projects/merge_requests/show/participants"
+ .col-md-3
+ %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
+ = cross_project_reference(@project, @merge_request)
+
.row
%section.col-md-9
- .votes-holder.pull-right
- #votes= render 'votes/votes_block', votable: @merge_request
- = render "projects/merge_requests/show/participants"
= render "projects/notes/notes_with_form"
%aside.col-md-3
.issuable-affix
- .clearfix
- %span.slead.has_tooltip{:"data-original-title" => 'Cross-project reference'}
- = cross_project_reference(@project, @merge_request)
- %hr
.context
= render 'shared/issuable/context', issuable: @merge_request
diff --git a/app/views/projects/merge_requests/_header_title.html.haml b/app/views/projects/merge_requests/_header_title.html.haml
new file mode 100644
index 00000000000..669a9b06bdf
--- /dev/null
+++ b/app/views/projects/merge_requests/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Merge Requests", namespace_project_merge_requests_path(@project.namespace, @project))
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 0bcd543fee7..25e4e8ba80d 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -14,11 +14,6 @@
%span
%i.fa.fa-ban
CLOSED
- - else
- %span.hidden-xs.hidden-sm
- %span.label-branch<
- %i.fa.fa-code-fork
- %span= merge_request.target_branch
- note_count = merge_request.mr_and_commit_notes.user.count
- if merge_request.assignee
&nbsp;
@@ -48,4 +43,4 @@
= merge_request.task_status
.pull-right.hidden-xs
- %small updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
+ %span updated #{time_ago_with_tooltip(merge_request.updated_at, placement: 'bottom', html_class: 'merge_request_updated_ago')}
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index b8a0ca9a42f..d86707b3d97 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -1,9 +1,8 @@
-.panel.panel-default
- %ul.well-list.mr-list
- = render @merge_requests
- - if @merge_requests.blank?
- %li
- .nothing-here-block No merge requests to show
+%ul.content-list.mr-list
+ = render @merge_requests
+ - if @merge_requests.blank?
+ %li
+ .nothing-here-block No merge requests to show
- if @merge_requests.present?
.pull-right
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 7709330611a..452006162db 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -37,7 +37,7 @@
%h4 Compare failed
%p We can't compare selected branches. It may be because of huge diff. Please try again or select different branches.
- else
- .light-well
+ .light-well.append-bottom-10
.center
%h4
There isn't anything to merge.
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 76f44211dac..6244d3ba0b4 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -1,10 +1,11 @@
%h3.page-title
New merge request
%p.slead
+ - source_title, target_title = format_mr_branch_names(@merge_request)
From
- %strong.label-branch #{@merge_request.source_project_namespace}:#{@merge_request.source_branch}
+ %strong.label-branch #{source_title}
%span into
- %strong.label-branch #{@merge_request.target_project_namespace}:#{@merge_request.target_branch}
+ %strong.label-branch #{target_title}
%span.pull-right
= link_to 'Change branches', mr_change_branches_path(@merge_request)
@@ -18,15 +19,13 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.nav.nav-tabs.merge-request-tabs
+ %ul.merge-request-tabs
%li.commits-tab
= link_to url_for(params), data: {target: '#commits', action: 'commits', toggle: 'tab'} do
- = icon('history')
Commits
%span.badge= @commits.size
%li.diffs-tab.active
= link_to url_for(params), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do
- = icon('list-alt')
Changes
%span.badge= @diffs.size
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index ec1838eb489..0b0f52c653c 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,14 +1,14 @@
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+= render "header_title"
+
- if params[:view] == 'parallel'
- fluid_layout true
.merge-request{'data-url' => merge_request_path(@merge_request)}
.merge-request-details.issuable-details
= render "projects/merge_requests/show/mr_title"
- %hr
= render "projects/merge_requests/show/mr_box"
- %hr
- .append-bottom-20.mr-source-target
+ .append-bottom-20.mr-source-target.prepend-top-default
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
@@ -39,20 +39,17 @@
= link_to "command line", "#modal_merge_info", class: "how_to_merge_link vlink", title: "How To Merge", "data-toggle" => "modal"
- if @commits.present?
- %ul.nav.nav-tabs.merge-request-tabs
+ %ul.merge-request-tabs
%li.notes-tab
= link_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#notes', action: 'notes', toggle: 'tab'} do
- = icon('comments')
Discussion
%span.badge= @merge_request.mr_and_commit_notes.user.count
%li.commits-tab
= link_to commits_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#commits', action: 'commits', toggle: 'tab'} do
- = icon('history')
Commits
%span.badge= @commits.size
%li.diffs-tab
= link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), data: {target: '#diffs', action: 'diffs', toggle: 'tab'} do
- = icon('list-alt')
Changes
%span.badge= @merge_request.diffs.size
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 7e5cb07f249..303ca0a880b 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,4 +1,6 @@
- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+= render "header_title"
+
%h3.page-title
= "Edit merge request ##{@merge_request.iid}"
%hr
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index 72fbe2e27a7..086298e5af1 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,7 +1,9 @@
- page_title "Merge Requests"
+= render "header_title"
+
= render 'projects/last_push'
-.append-bottom-10
- .pull-right
+.project-issuable-filter
+ .controls
= render 'shared/issuable/search_form', path: namespace_project_merge_requests_path(@project.namespace, @project)
- if can? current_user, :create_merge_request, @project
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index 15bd4e2fafd..fc03ee73a3d 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -1,4 +1,6 @@
- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+= render "header_title"
+
.merge-request
= render "projects/merge_requests/show/mr_title"
= render "projects/merge_requests/show/mr_box"
diff --git a/app/views/projects/merge_requests/new.html.haml b/app/views/projects/merge_requests/new.html.haml
index b038a640f67..9fdde80c6d9 100644
--- a/app/views/projects/merge_requests/new.html.haml
+++ b/app/views/projects/merge_requests/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Merge Request"
+= render "header_title"
+
- if @merge_request.can_be_created
= render 'new_submit'
- else
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index db1575f899a..f18cf96c17d 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -11,12 +11,12 @@
%pre.dark
- if @merge_request.for_fork?
:preserve
- git fetch #{@merge_request.source_project.http_url_to_repo} #{@merge_request.source_branch}
- git checkout -b #{@merge_request.source_project_path}-#{@merge_request.source_branch} FETCH_HEAD
+ git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch}
+ git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD
- else
:preserve
git fetch origin
- git checkout -b #{@merge_request.source_branch} origin/#{@merge_request.source_branch}
+ git checkout -b #{h @merge_request.source_branch} origin/#{h @merge_request.source_branch}
%p
%strong Step 2.
Review the changes locally
@@ -27,18 +27,18 @@
%pre.dark
- if @merge_request.for_fork?
:preserve
- git checkout #{@merge_request.target_branch}
- git merge --no-ff #{@merge_request.source_project_path}-#{@merge_request.source_branch}
+ git checkout #{h @merge_request.target_branch}
+ git merge --no-ff #{h @merge_request.source_project_path}-#{h @merge_request.source_branch}
- else
:preserve
- git checkout #{@merge_request.target_branch}
- git merge --no-ff #{@merge_request.source_branch}
+ git checkout #{h @merge_request.target_branch}
+ git merge --no-ff #{h @merge_request.source_branch}
%p
%strong Step 4.
Push the result of the merge to GitLab
%pre.dark
:preserve
- git push origin #{@merge_request.target_branch}
+ git push origin #{h @merge_request.target_branch}
- unless @merge_request.can_be_merged_by?(current_user)
%p
Note that pushing to GitLab requires write access to this repository.
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index e3cd4346872..b4f62a75890 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,11 +1,12 @@
-%h2.issue-title
- = gfm escape_once(@merge_request.title)
+.gray-content-block.middle-block
+ %h2.issue-title
+ = gfm escape_once(@merge_request.title)
-%div
- - if @merge_request.description.present?
- .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
- .wiki
- = preserve do
- = markdown(@merge_request.description)
- %textarea.hidden.js-task-list-field
- = @merge_request.description
+ %div
+ - if @merge_request.description.present?
+ .description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
+ .wiki
+ = preserve do
+ = markdown(@merge_request.description)
+ %textarea.hidden.js-task-list-field
+ = @merge_request.description
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index 9a1eb36fc88..2bf9cd597a4 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,10 +1,11 @@
-%h4.page-title
+.page-title
.issue-box{ class: issue_box_class(@merge_request) }
= @merge_request.state_human_name
- Merge Request ##{@merge_request.iid}
- %small.creator
+ %span.issue-id Merge Request ##{@merge_request.iid}
+ %span.creator
+ &middot;
+ created by #{link_to_member(@project, @merge_request.author, size: 24)}
&middot;
- created by #{link_to_member(@project, @merge_request.author)}
= time_ago_with_tooltip(@merge_request.created_at)
- if @merge_request.updated_at != @merge_request.created_at
%span
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 4d4e2f68f61..68dda1424cf 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -1,29 +1,44 @@
- if @merge_request.has_ci?
- .mr-widget-heading
- - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status|
- .ci_widget{class: "ci-#{status}", style: "display:none"}
- - if status == :success
- - status = "passed"
- = icon("check-circle")
- - else
- = icon("circle")
+ - ci_commit = @merge_request.source_project.ci_commit(@merge_request.source_sha)
+ - if ci_commit
+ - status = ci_commit.status
+ .mr-widget-heading
+ .ci_widget{class: "ci-#{status}"}
+ = ci_status_icon(ci_commit)
%span CI build #{status}
for #{@merge_request.last_commit_short_sha}.
%span.ci-coverage
- = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink"
+ = link_to "View build details", ci_status_path(ci_commit)
- .ci_widget
- = icon("spinner spin")
- Checking CI status for #{@merge_request.last_commit_short_sha}&hellip;
+ - else
+ - # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
+ - # Remove in later versions when services like Jenkins will set CI status via Commit status API
+ .mr-widget-heading
+ - [:success, :skipped, :canceled, :failed, :running, :pending].each do |status|
+ .ci_widget{class: "ci-#{status}", style: "display:none"}
+ - if status == :success
+ - status = "passed"
+ = icon("check-circle")
+ - else
+ = icon("circle")
+ %span CI build #{status}
+ for #{@merge_request.last_commit_short_sha}.
+ %span.ci-coverage
+ - if ci_build_details_path(@merge_request)
+ = link_to "View build details", ci_build_details_path(@merge_request), :"data-no-turbolink" => "data-no-turbolink"
- .ci_widget.ci-not_found{style: "display:none"}
- = icon("times-circle")
- Could not find CI status for #{@merge_request.last_commit_short_sha}.
+ .ci_widget
+ = icon("spinner spin")
+ Checking CI status for #{@merge_request.last_commit_short_sha}&hellip;
- .ci_widget.ci-error{style: "display:none"}
- = icon("times-circle")
- Could not connect to the CI server. Please check your settings and try again.
+ .ci_widget.ci-not_found{style: "display:none"}
+ = icon("times-circle")
+ Could not find CI status for #{@merge_request.last_commit_short_sha}.
- :coffeescript
- $ ->
- merge_request_widget.getCiStatus()
+ .ci_widget.ci-error{style: "display:none"}
+ = icon("times-circle")
+ Could not connect to the CI server. Please check your settings and try again.
+
+ :coffeescript
+ $ ->
+ merge_request_widget.getCiStatus()
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index d22dfa085b8..f223f687def 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -20,7 +20,7 @@
The changes were merged into
%span.label-branch= @merge_request.target_branch
You can remove the source branch now.
- = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
+ = link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-primary btn-sm remove_source_branch" do
%i.fa.fa-times
Remove Source Branch
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index b61e193fc42..613525437ab 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -9,7 +9,7 @@
= label_tag :should_remove_source_branch, class: "remove_source_checkbox" do
= check_box_tag :should_remove_source_branch
Remove source branch
- .accept-control
+ .accept-control.right
= link_to "#", class: "modify-merge-commit-link js-toggle-button" do
= icon('edit')
Modify commit message
diff --git a/app/views/projects/milestones/_form.html.haml b/app/views/projects/milestones/_form.html.haml
index b93462e5bdf..74e9668052d 100644
--- a/app/views/projects/milestones/_form.html.haml
+++ b/app/views/projects/milestones/_form.html.haml
@@ -21,7 +21,7 @@
.form-group.milestone-description
= f.label :description, "Description", class: "control-label"
.col-sm-10
- = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :description, classes: 'description form-control'
.hint
.pull-left Milestones are parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}.
diff --git a/app/views/projects/milestones/_header_title.html.haml b/app/views/projects/milestones/_header_title.html.haml
new file mode 100644
index 00000000000..5f4b6982a6d
--- /dev/null
+++ b/app/views/projects/milestones/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Milestones", namespace_project_milestones_path(@project.namespace, @project))
diff --git a/app/views/projects/milestones/_milestone.html.haml b/app/views/projects/milestones/_milestone.html.haml
index 2ce5358fa74..5e93d55b1fb 100644
--- a/app/views/projects/milestones/_milestone.html.haml
+++ b/app/views/projects/milestones/_milestone.html.haml
@@ -1,28 +1,34 @@
%li{class: "milestone milestone-#{milestone.closed? ? 'closed' : 'open'}", id: dom_id(milestone) }
- .pull-right
- - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
- = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-sm edit-milestone-link btn-grouped" do
- %i.fa.fa-pencil-square-o
- Edit
- = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-sm btn-close"
- = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-sm btn-remove" do
- %i.fa.fa-trash-o
- Remove
+ .row
+ .col-sm-6
+ %strong
+ = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- %h4
- = link_to_gfm truncate(milestone.title, length: 100), namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone)
- - if milestone.expired? and not milestone.closed?
- %span.cred (Expired)
- %small
- = milestone.expires_at
+ .col-sm-6
+ .pull-right.light #{milestone.percent_complete}% complete
.row
.col-sm-6
= link_to namespace_project_issues_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
= pluralize milestone.issues.count, 'Issue'
- &nbsp;
+ &middot;
= link_to namespace_project_merge_requests_path(milestone.project.namespace, milestone.project, milestone_title: milestone.title) do
= pluralize milestone.merge_requests.count, 'Merge Request'
- &nbsp;
- %span.light #{milestone.percent_complete}% complete
.col-sm-6
= milestone_progress_bar(milestone)
+
+ .row
+ .col-sm-6
+ - if milestone.expired? and not milestone.closed?
+ %span.cred (Expired)
+ - if milestone.expires_at
+ %span
+ = milestone.expires_at
+ .col-sm-6
+ - if can?(current_user, :admin_milestone, milestone.project) and milestone.active?
+ = link_to edit_namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), class: "btn btn-xs edit-milestone-link btn-grouped" do
+ %i.fa.fa-pencil-square-o
+ Edit
+ = link_to 'Close Milestone', namespace_project_milestone_path(@project.namespace, @project, milestone, milestone: {state_event: :close }), method: :put, remote: true, class: "btn btn-xs btn-close"
+ = link_to namespace_project_milestone_path(milestone.project.namespace, milestone.project, milestone), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-xs btn-remove" do
+ %i.fa.fa-trash-o
+ Remove
diff --git a/app/views/projects/milestones/edit.html.haml b/app/views/projects/milestones/edit.html.haml
index c09815a212a..e9dc0b77462 100644
--- a/app/views/projects/milestones/edit.html.haml
+++ b/app/views/projects/milestones/edit.html.haml
@@ -1,2 +1,3 @@
- page_title "Edit", @milestone.title, "Milestones"
+= render "header_title"
= render "form"
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index 995eecd7830..a207385bd43 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,18 +1,22 @@
- page_title "Milestones"
-.pull-right
- - if can? current_user, :admin_milestone, @project
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
- %i.fa.fa-plus
- New Milestone
+= render "header_title"
= render 'shared/milestones_filter'
+.gray-content-block
+ .pull-right
+ - if can? current_user, :admin_milestone, @project
+ = link_to new_namespace_project_milestone_path(@project.namespace, @project), class: "pull-right btn btn-new", title: "New Milestone" do
+ %i.fa.fa-plus
+ New Milestone
+ .oneline
+ Milestone allows you to group issues and set due date for it
+
.milestones
- .panel.panel-default
- %ul.well-list
- = render @milestones
+ %ul.content-list
+ = render @milestones
- - if @milestones.blank?
- %li
- .nothing-here-block No milestones to show
+ - if @milestones.blank?
+ %li
+ .nothing-here-block No milestones to show
= paginate @milestones, theme: "gitlab"
diff --git a/app/views/projects/milestones/new.html.haml b/app/views/projects/milestones/new.html.haml
index 47149dfea41..9ba9acb6f77 100644
--- a/app/views/projects/milestones/new.html.haml
+++ b/app/views/projects/milestones/new.html.haml
@@ -1,2 +1,3 @@
- page_title "New Milestone"
+= render "header_title"
= render "form"
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 7b1681df336..4eeb0621e52 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -1,4 +1,6 @@
- page_title @milestone.title, "Milestones"
+= render "header_title"
+
%h4.page-title
.issue-box{ class: issue_box_class(@milestone) }
- if @milestone.closed?
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 52b5b8b877e..16005161df6 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,4 +1,5 @@
- page_title "Network", @ref
+= header_title project_title(@project, "Network", namespace_project_network_path(@project.namespace, @project, current_ref))
= render "head"
.project-network
.controls
diff --git a/app/views/projects/network/show.json.erb b/app/views/projects/network/show.json.erb
index dc82adcb2c6..122e84b41b2 100644
--- a/app/views/projects/network/show.json.erb
+++ b/app/views/projects/network/show.json.erb
@@ -9,7 +9,7 @@
author: {
name: c.author_name,
email: c.author_email,
- icon: avatar_icon(c.author_email, 20)
+ icon: image_path(avatar_icon(c.author_email, 20))
},
time: c.time,
space: c.spaces.first,
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 636218368cc..bccea21e7a8 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -72,6 +72,11 @@
%i.fa.fa-google
Google Code
+ - if fogbugz_import_enabled?
+ = link_to new_import_fogbugz_path, class: 'btn import_fogbugz' do
+ %i.fa.fa-bug
+ Fogbugz
+
- if git_import_enabled?
= link_to "#", class: 'btn js-toggle-button import_git' do
%i.fa.fa-git
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index 8f7d2e84c70..a0e26f9827e 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -1,7 +1,7 @@
.note-edit-form
= form_for note, url: namespace_project_note_path(@project.namespace, @project, note), method: :put, remote: true, authenticity_token: true do |f|
= note_target_fields(note)
- = render layout: 'projects/md_preview', locals: { preview_class: 'note-text' } do
+ = render layout: 'projects/md_preview', locals: { preview_class: 'md-preview' } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text js-task-list-field'
= render 'projects/notes/hints'
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 3be8f44b282..d99445da59a 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -7,7 +7,7 @@
= f.hidden_field :noteable_id
= f.hidden_field :noteable_type
- = render layout: 'projects/md_preview', locals: { preview_class: "note-text", referenced_users: true } do
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :note, classes: 'note_text js-note-text'
= render 'projects/notes/hints'
.error-alert
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index de75d44fc41..1638ad6891a 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -1,11 +1,8 @@
%li.timeline-entry{ id: dom_id(note), class: [dom_class(note), "note-row-#{note.id}", ('system-note' if note.system)], data: { discussion: note.discussion_id } }
.timeline-entry-inner
.timeline-icon
- - if note.system
- %span= icon('circle')
- - else
- = link_to user_path(note.author) do
- = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: ''
+ = link_to user_path(note.author) do
+ = image_tag avatar_icon(note.author_email), class: 'avatar s40', alt: ''
.timeline-content
.note-header
- if note_editable?(note)
@@ -17,14 +14,10 @@
= icon('trash-o')
- unless note.system
- - member = note.project.team.find_member(note.author.id)
- - if member
+ - access = note.project.team.human_max_access(note.author.id)
+ - if access
%span.note-role.label
- = member.human_access
-
- - if note.system
- = link_to user_path(note.author) do
- = image_tag avatar_icon(note.author_email), class: 'avatar s16', alt: ''
+ = access
= link_to_member(note.project, note.author, avatar: false)
@@ -66,7 +59,9 @@
.note-text
= preserve do
= markdown(note.note, {no_header_anchors: true})
- = render 'projects/notes/edit_form', note: note
+ - unless note.system?
+ -# System notes can't be edited
+ = render 'projects/notes/edit_form', note: note
- if note.attachment.url
.note-attachment
diff --git a/app/views/projects/project_members/_header_title.html.haml b/app/views/projects/project_members/_header_title.html.haml
new file mode 100644
index 00000000000..a31f0a37fa2
--- /dev/null
+++ b/app/views/projects/project_members/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Members", namespace_project_project_members_path(@project.namespace, @project))
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index 6914543f6da..189906498cb 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -1,4 +1,6 @@
- page_title "Import members"
+= render "header_title"
+
%h3.page-title
Import members from another project
%p.light
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 162583e4b1d..9a0a824b811 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,30 +1,29 @@
- page_title "Members"
-%h3.page-title
- Users with access to this project
-
-%p.light
+= render "header_title"
+
+.gray-content-block.top-block
+ .clearfix.js-toggle-container
+ = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
+ .form-group
+ = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' }
+ = button_tag 'Search', class: 'btn'
+
+ - if can?(current_user, :admin_project_member, @project)
+ %span.pull-right
+ = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do
+ Add members
+ %i.fa.fa-chevron-down
+ = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
+ Import members
+
+ .js-toggle-content.hide.new-group-member-holder
+ = render "new_project_member"
+
+%p.prepend-top-default.light
+ Users with access to this project are listed below.
Read more about project permissions
%strong= link_to "here", help_page_path("permissions", "permissions"), class: "vlink"
-%hr
-
-.clearfix.js-toggle-container
- = form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
- .form-group
- = search_field_tag :search, params[:search], { placeholder: 'Find existing member by name', class: 'form-control search-text-input' }
- = button_tag 'Search', class: 'btn'
-
- - if can?(current_user, :admin_project_member, @project)
- %span.pull-right
- = button_tag class: 'btn btn-new btn-grouped js-toggle-button', type: 'button' do
- Add members
- %i.fa.fa-chevron-down
- = link_to import_namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-grouped", title: "Import members from another project" do
- Import members
-
- .js-toggle-content.hide.new-group-member-holder
- = render "new_project_member"
-
= render "team", members: @project_members
- if @group
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
new file mode 100644
index 00000000000..e6b8a2e6fe7
--- /dev/null
+++ b/app/views/projects/runners/_runner.html.haml
@@ -0,0 +1,34 @@
+%li.runner{id: dom_id(runner)}
+ %h4
+ = runner_status_icon(runner)
+ %span.monospace
+ - if @runners.include?(runner)
+ = link_to runner.short_sha, runner_path(runner)
+ %small
+ =link_to edit_namespace_project_runner_path(@project.namespace, @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', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - else
+ - runner_project = @ci_project.runner_projects.find_by(runner_id: runner)
+ = link_to 'Disable for this project', [:ci, @ci_project, runner_project], data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ - elsif runner.specific?
+ = form_for [:ci, @ci_project, @ci_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/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
new file mode 100644
index 00000000000..316ea747b14
--- /dev/null
+++ b/app/views/projects/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 @ci_project.shared_runners_enabled
+ = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-warning', method: :post do
+ Disable shared runners
+ - else
+ = link_to toggle_shared_runners_ci_project_path(@ci_project), class: 'btn btn-success', method: :post do
+ Enable shared runners
+ &nbsp; 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 partial: 'runner', collection: @shared_runners, as: :runner
+ - if @shared_runners_count > 10
+ .light
+ and #{@shared_runners_count - 10} more...
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
new file mode 100644
index 00000000000..c13625c7e49
--- /dev/null
+++ b/app/views/projects/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 #{@ci_project.token}
+ %li
+ Start runner!
+
+
+- if @runners.any?
+ %h4.underlined-title Runners activated for this project
+ %ul.bordered-list.activated-specific-runners
+ = render partial: 'runner', collection: @runners, as: :runner
+
+- if @specific_runners.any?
+ %h4.underlined-title Available specific runners
+ %ul.bordered-list.available-specific-runners
+ = render partial: 'runner', collection: @specific_runners, as: :runner
+ = paginate @specific_runners
diff --git a/app/views/projects/runners/edit.html.haml b/app/views/projects/runners/edit.html.haml
new file mode 100644
index 00000000000..66851d38316
--- /dev/null
+++ b/app/views/projects/runners/edit.html.haml
@@ -0,0 +1,27 @@
+%h4 Runner ##{@runner.id}
+%hr
+= form_for @runner, url: runner_path(@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/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
new file mode 100644
index 00000000000..529fb9c296d
--- /dev/null
+++ b/app/views/projects/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/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
new file mode 100644
index 00000000000..ffec495f85a
--- /dev/null
+++ b/app/views/projects/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/projects/show.html.haml b/app/views/projects/show.html.haml
index 507f2c7beb0..efa119edd5a 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -11,10 +11,10 @@
= render "home_panel"
-.project-stats
+.project-stats.gray-content-block
%ul.nav.nav-pills
%li
- = link_to namespace_project_commits_path(@project.namespace, @project, @ref || @repository.root_ref) do
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
= pluralize(number_with_delimiter(@project.commit_count), 'commit')
%li
= link_to namespace_project_branches_path(@project.namespace, @project) do
@@ -24,7 +24,7 @@
= pluralize(number_with_delimiter(@repository.tag_names.count), 'tag')
%li
- = link_to namespace_project_path(@project.namespace, @project) do
+ = link_to project_files_path(@project) do
= repository_size
- if !prefer_readme? && @repository.readme
@@ -63,21 +63,22 @@
= icon("exclamation-triangle fw")
Archived project! Repository is read-only
-%hr
%section
- if prefer_readme?
- = render 'projects/readme'
+ .project-show-readme
+ = render 'projects/readme'
- else
- = render 'projects/activity'
+ .project-show-activity
+ = render 'projects/activity'
- if current_user
- access = user_max_access_in_project(current_user, @project)
- if access
- %hr
- %p.light
- You have #{access} access to this project.
- - if @project.project_member_by_id(current_user)
- = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
- data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do
- Leave this project \ No newline at end of file
+ .prepend-top-20.project-footer
+ .gray-content-block.footer-block.center
+ You have #{access} access to this project.
+ - if @project.project_member_by_id(current_user)
+ = link_to leave_namespace_project_project_members_path(@project.namespace, @project),
+ data: { confirm: leave_project_message(@project) }, method: :delete, title: 'Leave project', class: 'cred' do
+ Leave this project
diff --git a/app/views/projects/snippets/_header_title.html.haml b/app/views/projects/snippets/_header_title.html.haml
new file mode 100644
index 00000000000..04f0bbe9853
--- /dev/null
+++ b/app/views/projects/snippets/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, "Snippets", namespace_project_snippets_path(@project.namespace, @project))
diff --git a/app/views/projects/snippets/edit.html.haml b/app/views/projects/snippets/edit.html.haml
index 945f0084dff..e69f2d99709 100644
--- a/app/views/projects/snippets/edit.html.haml
+++ b/app/views/projects/snippets/edit.html.haml
@@ -1,4 +1,6 @@
- page_title "Edit", @snippet.title, "Snippets"
+= render "header_title"
+
%h3.page-title
Edit snippet
%hr
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 45d4de6a385..3fed2c9949d 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,4 +1,6 @@
- page_title "Snippets"
+= render "header_title"
+
%h3.page-title
Snippets
- if can? current_user, :create_project_snippet, @project
diff --git a/app/views/projects/snippets/new.html.haml b/app/views/projects/snippets/new.html.haml
index e38d95c45e7..67cd69fd215 100644
--- a/app/views/projects/snippets/new.html.haml
+++ b/app/views/projects/snippets/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Snippets"
+= render "header_title"
+
%h3.page-title
New snippet
%hr
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 8cbb813c758..be7d4d486fa 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -1,4 +1,6 @@
- page_title @snippet.title, "Snippets"
+= render "header_title"
+
%h3.page-title
= @snippet.title
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 28ad272322f..2ca295fc5f3 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,13 +1,14 @@
- commit = @repository.commit(tag.target)
%li
- %h4
+ %div
= link_to namespace_project_commits_path(@project.namespace, @project, tag.name), class: "" do
- %i.fa.fa-tag
- = tag.name
+ %strong
+ %i.fa.fa-tag
+ = tag.name
- if tag.message.present?
&nbsp;
= strip_gpg_signature(tag.message)
- .pull-right
+ .controls
- if can? current_user, :download_code, @project
= render 'projects/repositories/download_archive', ref: tag.name, btn_class: 'btn-grouped btn-group-xs'
- if can?(current_user, :admin_project, @project)
@@ -15,8 +16,7 @@
%i.fa.fa-trash-o
- if commit
- %ul.list-unstyled
- = render 'projects/commits/inline_commit', commit: commit, project: @project
+ = render 'projects/branches/commit', commit: commit, project: @project
- else
%p
Cant find HEAD commit for this tag
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index d4652a47cba..85d76eae3b5 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -1,21 +1,19 @@
- page_title "Tags"
+= render "projects/commits/header_title"
= render "projects/commits/head"
-%h3.page-title
- Git Tags
+.gray-content-block
- if can? current_user, :push_code, @project
.pull-right
= link_to new_namespace_project_tag_path(@project.namespace, @project), class: 'btn btn-create new-tag-btn' do
%i.fa.fa-add-sign
New tag
-
-%p.light
- Tags give the ability to mark specific points in history as being important
-%hr
+ .oneline
+ Tags give the ability to mark specific points in history as being important
.tags
- unless @tags.empty?
- %ul.bordered-list
+ %ul.content-list
- @tags.each do |tag|
= render 'tag', tag: @repository.find_tag(tag)
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 172fafdeeff..9f5c1be125c 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -1,4 +1,6 @@
- page_title "New Tag"
+= render "projects/commits/header_title"
+
- if @error
.alert.alert-danger
%button{ type: "button", class: "close", "data-dismiss" => "alert"} &times;
diff --git a/app/views/projects/tree/_tree.html.haml b/app/views/projects/tree/_tree.html.haml
index 5048154cb2f..367a87927d7 100644
--- a/app/views/projects/tree/_tree.html.haml
+++ b/app/views/projects/tree/_tree.html.haml
@@ -14,7 +14,7 @@
%small
%i.fa.fa-plus
-%div#tree-content-holder.tree-content-holder
+%div#tree-content-holder.tree-content-holder.prepend-top-20
%table#tree-slider{class: "table_#{@hex_path} tree-table" }
%thead
%tr
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index c9e59428e78..dec4677f830 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -1,4 +1,5 @@
- page_title @path.presence || "Files", @ref
+- header_title project_title(@project, "Files", project_files_path(@project))
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
diff --git a/app/views/projects/triggers/_trigger.html.haml b/app/views/projects/triggers/_trigger.html.haml
new file mode 100644
index 00000000000..48b3b5c9920
--- /dev/null
+++ b/app/views/projects/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', namespace_project_trigger_path(@project.namespace, @project, trigger), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-danger btn-sm btn-grouped"
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
new file mode 100644
index 00000000000..17dcb78e256
--- /dev/null
+++ b/app/views/projects/triggers/index.html.haml
@@ -0,0 +1,67 @@
+%h3.page-title
+ 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 partial: 'trigger', collection: @triggers, as: :trigger
+- else
+ %h4 No triggers
+
+= form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create'), 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(@ci_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(@ci_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(@ci_project.id, 'REF_NAME')}
diff --git a/app/views/projects/variables/show.html.haml b/app/views/projects/variables/show.html.haml
new file mode 100644
index 00000000000..29416a94ff6
--- /dev/null
+++ b/app/views/projects/variables/show.html.haml
@@ -0,0 +1,39 @@
+%h3.page-title
+ 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 @ci_project, url: url_for(controller: 'projects/variables', action: 'update'), html: { class: 'form-horizontal' } do |f|
+ - if @project.errors.any?
+ #error_explanation
+ %p.lead= "#{pluralize(@ci_project.errors.count, "error")} prohibited this project from being saved:"
+ .alert.alert-error
+ %ul
+ - @ci_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/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 904600499ae..05d754adbe5 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -21,7 +21,7 @@
.form-group.wiki-content
= f.label :content, class: 'control-label'
.col-sm-10
- = render layout: 'projects/md_preview', locals: { preview_class: "wiki" } do
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview" } do
= render 'projects/zen', f: f, attr: :content, classes: 'description form-control'
.col-sm-12.hint
.pull-left Wiki content is parsed with #{link_to "GitLab Flavored Markdown", help_page_path("markdown", "markdown"), target: '_blank'}
diff --git a/app/views/projects/wikis/_header_title.html.haml b/app/views/projects/wikis/_header_title.html.haml
new file mode 100644
index 00000000000..408adc36ca6
--- /dev/null
+++ b/app/views/projects/wikis/_header_title.html.haml
@@ -0,0 +1 @@
+- header_title project_title(@project, 'Wiki', get_project_wiki_path(@project))
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 788bb8cf1e2..14f25822259 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,8 +1,15 @@
%span.pull-right
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new btn-grouped", "data-toggle" => "modal" do
+ %i.fa.fa-plus
+ New Page
+
- if (@page && @page.persisted?)
- = link_to history_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ = link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
Page History
- if can?(current_user, :create_wiki, @project)
- = link_to edit_namespace_project_wiki_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
+ = link_to namespace_project_wiki_edit_path(@project.namespace, @project, @page), class: "btn btn-grouped" do
%i.fa.fa-pencil-square-o
Edit
+
+= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index 804a1b52dbe..fffb4eb31ab 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,19 +1,10 @@
-%ul.nav.nav-tabs
+%ul.center-top-menu
= nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
= link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
= nav_link(path: 'wikis#pages') do
- = link_to 'Pages', pages_namespace_project_wikis_path(@project.namespace, @project)
+ = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
= nav_link(path: 'wikis#git_access') do
- = link_to git_access_namespace_project_wikis_path(@project.namespace, @project) do
- %i.fa.fa-download
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
Git Access
-
- - if can?(current_user, :create_wiki, @project)
- .pull-right
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- %i.fa.fa-plus
- New Page
-
-= render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index 3f1dce1050c..0b709c3695b 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,4 +1,6 @@
- page_title "Edit", @page.title, "Wiki"
+= render "header_title"
+
= render 'nav'
.pull-right
= render 'main_links'
diff --git a/app/views/projects/wikis/empty.html.haml b/app/views/projects/wikis/empty.html.haml
index ead99412406..c7e490c3cd1 100644
--- a/app/views/projects/wikis/empty.html.haml
+++ b/app/views/projects/wikis/empty.html.haml
@@ -1,4 +1,6 @@
- page_title "Wiki"
+= render "header_title"
+
%h3.page-title Empty page
%hr
.error_message
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index 825f2a161c4..6417ef4a38b 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,15 +1,18 @@
- page_title "Git Access", "Wiki"
+= render "header_title"
+
= render 'nav'
-.row
- .col-sm-6
- %h3.page-title
- Git access for
- %strong= @project_wiki.path_with_namespace
+.gray-content-block
+ .row
+ .col-sm-6
+ %h3.page-title
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .col-sm-6
- = render "shared/clone_panel", project: @project_wiki
+ .col-sm-6
+ = render "shared/clone_panel", project: @project_wiki
-.git-empty
+.git-empty.prepend-top-default
%fieldset
%legend Install Gollum:
%pre.dark
@@ -20,7 +23,7 @@
%pre.dark
:preserve
git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
- cd #{@project_wiki.path}
+ cd #{h @project_wiki.path}
%legend Start Gollum And Edit Locally:
%pre.dark
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 673ec2d20e5..bfbef823b35 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,8 +1,11 @@
-- page_title "History", @page.title, "Wiki"
+- page_title "History", @page.title.capitalize, "Wiki"
+= render "header_title"
+
= render 'nav'
-%h3.page-title
- %span.light History for
- = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
+.gray-content-block
+ %h3.page-title
+ %span.light History for
+ = link_to @page.title, namespace_project_wiki_path(@project.namespace, @project, @page)
%table.table
%thead
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 890ff1aed73..03e6a522b25 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,8 +1,11 @@
- page_title "All Pages", "Wiki"
+= render "header_title"
+
= render 'nav'
-%h3.page-title
- All Pages
-%ul.bordered-list
+.gray-content-block
+ %h3.page-title
+ All Pages
+%ul.content-list
- @wiki_pages.each do |wiki_page|
%li
%h4
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 5c4dd7f91ae..55fbf5a8b6e 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,25 +1,28 @@
-- page_title @page.title, "Wiki"
+- page_title @page.title.capitalize, "Wiki"
+= render "header_title"
+
= render 'nav'
-%h3.page-title
- = @page.title
+
+.gray-content-block
= render 'main_links'
+ %h3.page-title
+ = @page.title.capitalize
-.wiki-last-edit-by
- Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ .wiki-last-edit-by
+ Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
- if @page.historical?
.warning_message
This is an old version of this page.
- You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", history_namespace_project_wiki_path(@project.namespace, @project, @page)}.
+ You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
-%hr
-.wiki-holder
+.wiki-holder.prepend-top-default
.wiki
= preserve do
= render_wiki_content(@page)
-%hr
-.wiki-last-edit-by
- Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+.gray-content-block.footer-block
+ .wiki-last-edit-by
+ Last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 07672359dba..b23b2f0d5eb 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -4,7 +4,7 @@
.input-group-btn
%button{ |
type: 'button', |
- class: "btn btn-sm #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
+ class: "btn #{ 'active' if default_clone_protocol == 'ssh' }#{ ' has_tooltip' if current_user && current_user.require_ssh_key? }", |
:"data-clone" => project.ssh_url_to_repo, |
:"data-title" => "Add an SSH key to your profile<br> to pull or push via SSH",
:"data-html" => "true",
@@ -13,15 +13,15 @@
.input-group-btn
%button{ |
type: 'button', |
- class: "btn btn-sm #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
+ class: "btn #{ 'active' if default_clone_protocol == 'http' }#{ ' has_tooltip' if current_user && current_user.require_password? }", |
:"data-clone" => project.http_url_to_repo, |
:"data-title" => "Set a password on your account<br> to pull or push via #{gitlab_config.protocol.upcase}",
:"data-html" => "true",
:"data-container" => "body"}
= gitlab_config.protocol.upcase
- = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control input-sm", readonly: true
+ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true
- if project.kind_of?(Project)
.input-group-addon
.visibility-level-label.has_tooltip{'data-title' => "#{visibility_level_label(project.visibility_level)} project" }
= visibility_level_icon(project.visibility_level)
- = visibility_level_label(project.visibility_level).downcase
+
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 334db60690d..8495774accc 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,4 @@
-%ul.nav.nav-pills.event_filter
+.btn-group.btn-group-next.event-filter
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index 45ec49280d2..8d6e16f74c3 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -8,7 +8,10 @@
- help = field[:help]
.form-group
- = form.label name, title, class: "control-label"
+ - if type == "password" && value.present?
+ = form.label name, "Change #{title}", class: "control-label"
+ - else
+ = form.label name, title, class: "control-label"
.col-sm-10
- if type == 'text'
= form.text_field name, class: "form-control", placeholder: placeholder
@@ -19,6 +22,6 @@
- elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- elsif type == 'password'
- = form.password_field name, value: value, class: 'form-control'
+ = form.password_field name, autocomplete: "new-password", class: 'form-control'
- if help
%span.help-block= help
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index f685ae7726c..cbdecda4fff 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,14 +1,11 @@
-.milestones-filters.append-bottom-10
- %ul.nav.nav-tabs
+.milestones-filters
+ %ul.center-top-menu
%li{class: ("active" if params[:state].blank? || params[:state] == 'opened')}
= link_to milestones_filter_path(state: 'opened') do
- %i.fa.fa-exclamation-circle
Open
%li{class: ("active" if params[:state] == 'closed')}
= link_to milestones_filter_path(state: 'closed') do
- %i.fa.fa-check-circle
Closed
%li{class: ("active" if params[:state] == 'all')}
= link_to milestones_filter_path(state: 'all') do
- %i.fa.fa-compass
All
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 229ae359bc5..a54c5fa8c33 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -1,23 +1,21 @@
- group_member = local_assigns[:group_member]
%li
- if group_member
- .pull-right.hidden-xs
+ .controls.hidden-xs
- if can?(current_user, :admin_group, group)
= link_to edit_group_path(group), class: "btn-sm btn btn-grouped" do
%i.fa.fa-cogs
- Settings
= link_to leave_group_group_members_path(group), data: { confirm: leave_group_message(group.name) }, method: :delete, class: "btn-sm btn btn-grouped", title: 'Leave this group' do
%i.fa.fa-sign-out
- Leave
- = image_tag group_icon(group), class: "avatar s40 avatar-tile hidden-xs"
+ = image_tag group_icon(group), class: "avatar s46 hidden-xs"
= link_to group, class: 'group-name' do
%strong= group.name
- if group_member
as
- %strong #{group_member.human_access}
+ %span #{group_member.human_access}
%div.light
#{pluralize(group.projects.count, "project")}, #{pluralize(group.users.count, "user")}
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index bcaa48c7a12..8f16773077e 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,33 +1,28 @@
.issues-filters
.issues-state-filters
- %ul.nav.nav-tabs
+ %ul.center-top-menu
%li{class: ("active" if params[:state] == 'opened')}
= link_to page_filter_path(state: 'opened') do
- = icon('exclamation-circle')
#{state_filters_text_for(:opened, @project)}
- if defined?(type) && type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')}
= link_to page_filter_path(state: 'merged') do
- = icon('check-circle')
#{state_filters_text_for(:merged, @project)}
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed') do
- = icon('ban')
#{state_filters_text_for(:closed, @project)}
- else
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed') do
- = icon('check-circle')
#{state_filters_text_for(:closed, @project)}
%li{class: ("active" if params[:state] == 'all')}
= link_to page_filter_path(state: 'all') do
- = icon('compass')
#{state_filters_text_for(:all, @project)}
- .issues-details-filters
+ .issues-details-filters.gray-content-block
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name]), method: :get, class: 'filter-form' do
- if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
.check-all-holder
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 09327d645f3..33ec726e93c 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -9,7 +9,7 @@
= f.label :title, class: 'control-label' do
%strong= 'Title *'
.col-sm-10
- = f.text_field :title, maxlength: 255, autofocus: true,
+ = f.text_field :title, maxlength: 255, autofocus: true, autocomplete: 'off',
class: 'form-control pad js-gfm-input', required: true
- if issuable.is_a?(MergeRequest)
@@ -24,7 +24,7 @@
= f.label :description, 'Description', class: 'control-label'
.col-sm-10
- = render layout: 'projects/md_preview', locals: { preview_class: "wiki", referenced_users: true } do
+ = render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description,
classes: 'description form-control'
.col-sm-12.hint
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 021e3b689a1..16e1d8421de 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -1,14 +1,15 @@
- projects_limit = 20 unless local_assigns[:projects_limit]
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
+- ci = false unless local_assigns[:ci] == true
%ul.projects-list
- projects.each_with_index do |project, i|
- css_class = (i >= projects_limit) ? 'hide' : nil
= render "shared/projects/project", project: project,
- avatar: avatar, stars: stars, css_class: css_class
+ avatar: avatar, stars: stars, css_class: css_class, ci: ci
- - if projects.count > projects_limit
+ - if projects.size > projects_limit
%li.bottom.center
.light
#{projects_limit} of #{pluralize(projects.count, 'project')} displayed.
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 4bfdf4d55ff..e67e5a8a638 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -1,12 +1,14 @@
- avatar = true unless local_assigns[:avatar] == false
- stars = true unless local_assigns[:stars] == false
-- css_class = nil unless local_assigns[:css_class]
+- ci = false unless local_assigns[:ci] == true
+- css_class = '' unless local_assigns[:css_class]
+- css_class += " no-description" unless project.description.present?
%li.project-row{ class: css_class }
- = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2'] do
+ = cache [project.namespace, project, controller.controller_name, controller.action_name, 'v2.2'] do
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ = project_icon(project, alt: '', class: 'avatar project-avatar s46')
%span.project-full-name
%span.namespace-name
- if project.namespace
@@ -14,8 +16,16 @@
\/
%span.project-name.filter-title
= project.name
+
+ .project-controls
+ - if ci && !project.empty_repo? && project.commit
+ - if ci_commit = project.ci_commit(project.commit.sha)
+ = link_to ci_status_path(ci_commit), class: "c#{ci_status_color(ci_commit)}",
+ title: "Build status: #{ci_commit.status}", data: {toggle: 'tooltip', placement: 'left'} do
+ = ci_status_icon(ci_commit)
+ &nbsp;
- if stars
- %span.pull-right.light
+ %span
%i.fa.fa-star
= project.star_count
- if project.description.present?
diff --git a/app/views/snippets/_head.html.haml b/app/views/snippets/_head.html.haml
deleted file mode 100644
index 0adf6b91f2c..00000000000
--- a/app/views/snippets/_head.html.haml
+++ /dev/null
@@ -1,7 +0,0 @@
-%ul.center-top-menu
- = nav_link(page: user_snippets_path(current_user), html_options: {class: 'home'}) do
- = link_to user_snippets_path(current_user), title: 'Your snippets', data: {placement: 'right'} do
- Your Snippets
- = nav_link(page: snippets_path) do
- = link_to snippets_path, title: 'Explore snippets', data: {placement: 'right'} do
- Explore Snippets
diff --git a/app/views/snippets/current_user_index.html.haml b/app/views/snippets/current_user_index.html.haml
deleted file mode 100644
index 62a967a2e06..00000000000
--- a/app/views/snippets/current_user_index.html.haml
+++ /dev/null
@@ -1,35 +0,0 @@
-- page_title "Your Snippets"
-= render 'head'
-
-.slead
- Share code pastes with others out of git repository
-
- .pull-right
- = link_to new_snippet_path, class: "btn btn-new btn-sm", title: "New Snippet" do
- Add new snippet
-
-%ul.nav.nav-tabs
- = nav_tab :scope, nil do
- = link_to user_snippets_path(@user) do
- All
- %span.badge
- = @user.snippets.count
- = nav_tab :scope, 'are_private' do
- = link_to user_snippets_path(@user, scope: 'are_private') do
- Private
- %span.badge
- = @user.snippets.are_private.count
- = nav_tab :scope, 'are_internal' do
- = link_to user_snippets_path(@user, scope: 'are_internal') do
- Internal
- %span.badge
- = @user.snippets.are_internal.count
- = nav_tab :scope, 'are_public' do
- = link_to user_snippets_path(@user, scope: 'are_public') do
- Public
- %span.badge
- = @user.snippets.are_public.count
-
-.my-snippets
- = render 'snippets'
-
diff --git a/app/views/snippets/index.html.haml b/app/views/snippets/index.html.haml
index 8608815e0a6..7e4918a6085 100644
--- a/app/views/snippets/index.html.haml
+++ b/app/views/snippets/index.html.haml
@@ -1,9 +1,13 @@
-- page_title "Public Snippets"
-- if current_user
- = render 'head'
+- page_title "By #{@user.name}", "Snippets"
-.slead
- Public snippets created by you and other users are listed here
+%ol.breadcrumb
+ %li
+ = link_to snippets_path do
+ Snippets
+ %li
+ = @user.name
+ .pull-right.hidden-xs
+ = link_to user_path(@user) do
+ #{@user.name} profile page
= render 'snippets'
-
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index aed00f9caeb..97374e073dc 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -20,10 +20,10 @@
.back-link
- if @snippet.author == current_user
- = link_to user_snippets_path(current_user) do
+ = link_to dashboard_snippets_path do
&larr; your snippets
- else
- = link_to snippets_path do
+ = link_to explore_snippets_path do
&larr; explore snippets
.file-holder
diff --git a/app/views/snippets/user_index.html.haml b/app/views/snippets/user_index.html.haml
deleted file mode 100644
index 7af5352da34..00000000000
--- a/app/views/snippets/user_index.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- page_title "Snippets", @user.name
-
-%ol.breadcrumb
- %li
- = link_to snippets_path do
- Snippets
- %li
- = @user.name
- .pull-right.hidden-xs
- = link_to user_path(@user) do
- #{@user.name} profile page
-
-= render 'snippets'
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index aa4e8722fb1..11beb3e3239 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -16,17 +16,16 @@
- if @user == current_user
.pull-right.hidden-xs
= link_to profile_path, class: 'btn btn-sm' do
- %i.fa.fa-pencil-square-o
- Edit Profile settings
+ = icon('user')
+ Profile settings
- elsif current_user
- .pull-right
- %span.dropdown
- %a.light.dropdown-toggle.btn.btn-sm{href: '#', "data-toggle" => "dropdown"}
+ .report_abuse.pull-right
+ - if @user.abuse_report
+ %span#report_abuse_btn.light.btn.btn-sm.btn-close{title: 'Already reported for abuse', data: {toggle: 'tooltip', placement: 'right', container: 'body'}}
+ = icon('exclamation-circle')
+ - else
+ %a.light.btn.btn-sm{href: new_abuse_report_path(user_id: @user.id), title: 'Report abuse', data: {toggle: 'tooltip', placement: 'right', container: 'body'}}
= icon('exclamation-circle')
- %ul.dropdown-menu.dropdown-menu-right
- %li
- = link_to new_abuse_report_path(user_id: @user.id) do
- Report abuse
.username
@#{@user.username}
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/app/workers/email_receiver_worker.rb b/app/workers/email_receiver_worker.rb
index 8cfb96ef376..5a921a73fe9 100644
--- a/app/workers/email_receiver_worker.rb
+++ b/app/workers/email_receiver_worker.rb
@@ -4,7 +4,7 @@ class EmailReceiverWorker
sidekiq_options queue: :incoming_email
def perform(raw)
- return unless Gitlab::ReplyByEmail.enabled?
+ return unless Gitlab::IncomingEmail.enabled?
begin
Gitlab::Email::Receiver.new(raw).execute
diff --git a/app/workers/fork_registration_worker.rb b/app/workers/fork_registration_worker.rb
deleted file mode 100644
index fffa8b3a659..00000000000
--- a/app/workers/fork_registration_worker.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class ForkRegistrationWorker
- include Sidekiq::Worker
-
- sidekiq_options queue: :default
-
- def perform(from_project_id, to_project_id, private_token)
- from_project = Project.find(from_project_id)
- to_project = Project.find(to_project_id)
-
- from_project.gitlab_ci_service.fork_registration(to_project, private_token)
- end
-end
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
new file mode 100644
index 00000000000..acd1c43f06b
--- /dev/null
+++ b/app/workers/repository_fork_worker.rb
@@ -0,0 +1,34 @@
+class RepositoryForkWorker
+ include Sidekiq::Worker
+ include Gitlab::ShellAdapter
+
+ sidekiq_options queue: :gitlab_shell
+
+ def perform(project_id, source_path, target_path)
+ project = Project.find_by_id(project_id)
+
+ unless project.present?
+ logger.error("Project #{project_id} no longer exists!")
+ return
+ end
+
+ result = gitlab_shell.fork_repository(source_path, target_path)
+
+ unless result
+ logger.error("Unable to fork project #{project_id} for repository #{source_path} -> #{target_path}")
+ project.import_fail
+ project.save
+ return
+ end
+
+ if project.valid_repo?
+ ProjectCacheWorker.perform_async(project.id)
+ project.import_finish
+ else
+ project.import_fail
+ logger.error("Project #{id} had an invalid repository after fork")
+ end
+
+ project.save
+ end
+end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index f2ba2e15e7b..ea2808045eb 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -7,22 +7,31 @@ class RepositoryImportWorker
def perform(project_id)
project = Project.find(project_id)
- import_result = gitlab_shell.send(:import_repository,
+ unless project.import_url == Project::UNKNOWN_IMPORT_URL
+ import_result = gitlab_shell.send(:import_repository,
project.path_with_namespace,
project.import_url)
- return project.import_fail unless import_result
+ return project.import_fail unless import_result
+ else
+ unless project.create_repository
+ return project.import_fail
+ end
+ end
- data_import_result = if project.import_type == 'github'
- Gitlab::GithubImport::Importer.new(project).execute
- elsif project.import_type == 'gitlab'
- Gitlab::GitlabImport::Importer.new(project).execute
- elsif project.import_type == 'bitbucket'
- Gitlab::BitbucketImport::Importer.new(project).execute
- elsif project.import_type == 'google_code'
- Gitlab::GoogleCodeImport::Importer.new(project).execute
- else
- true
- end
+ data_import_result = case project.import_type
+ when 'github'
+ Gitlab::GithubImport::Importer.new(project).execute
+ when 'gitlab'
+ Gitlab::GitlabImport::Importer.new(project).execute
+ when 'bitbucket'
+ Gitlab::BitbucketImport::Importer.new(project).execute
+ when 'google_code'
+ Gitlab::GoogleCodeImport::Importer.new(project).execute
+ when 'fogbugz'
+ Gitlab::FogbugzImport::Importer.new(project).execute
+ else
+ true
+ end
return project.import_fail unless data_import_result
Gitlab::BitbucketImport::KeyDeleter.new(project).execute if project.import_type == 'bitbucket'
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/tmp/.gitkeep b/builds/.gitkeep
index e69de29bb2d..e69de29bb2d 100644
--- a/tmp/.gitkeep
+++ b/builds/.gitkeep
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 @@ Gitlab::Application.configure do
# 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.yml.example b/config/gitlab.yml.example
index c7b60a1d4b1..4f7f0b6ef19 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -4,7 +4,7 @@
#
########################### NOTE #####################################
# This file should not receive new settings. All configuration options #
-# that do not require application restart are being moved to #
+# that do not require an application restart are being moved to #
# ApplicationSetting model! #
# If you change this file in a Merge Request, please also create #
# a MR on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests #
@@ -96,10 +96,10 @@ production: &base
## Reply by email
# Allow users to comment on issues and merge requests by replying to notification emails.
- # For documentation on how to set this up, see http://doc.gitlab.com/ce/reply_by_email/README.md
- reply_by_email:
+ # For documentation on how to set this up, see http://doc.gitlab.com/ce/incoming_email/README.html
+ incoming_email:
enabled: false
- address: "replies+%{reply_key}@gitlab.example.com"
+ address: "incoming+%{key}@gitlab.example.com"
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
@@ -110,7 +110,23 @@ production: &base
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
#
- # 2. Auth settings
+ # 2. GitLab CI settings
+ # ==========================
+
+ gitlab_ci:
+ # 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/
+
+ #
+ # 3. Auth settings
# ==========================
## LDAP settings
@@ -180,6 +196,26 @@ production: &base
#
user_filter: ''
+ # LDAP attributes that GitLab will use to create an account for the LDAP user.
+ # The specified attribute can either be the attribute name as a string (e.g. 'mail'),
+ # or an array of attribute names to try in order (e.g. ['mail', 'email']).
+ # Note that the user's LDAP login will always be the attribute specified as `uid` above.
+ attributes:
+ # The username will be used in paths for the user's own projects
+ # (like `gitlab.example.com/username/project`) and when mentioning
+ # them in issues, merge request and comments (like `@username`).
+ # If the attribute specified for `username` contains an email address,
+ # the GitLab username will be the part of the email address before the '@'.
+ username: ['uid', 'userid', 'sAMAccountName']
+ email: ['mail', 'email', 'userPrincipalName']
+
+ # If no full name could be found at the attribute specified for `name`,
+ # the full name is determined using the attributes specified for
+ # `first_name` and `last_name`.
+ name: 'cn'
+ first_name: 'givenName'
+ last_name: 'sn'
+
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
# so that GitLab can remember which LDAP server a user belongs to.
@@ -216,28 +252,28 @@ production: &base
# arguments, followed by optional 'args' which can be either a hash or an array.
# Documentation for this is available at http://doc.gitlab.com/ce/integration/omniauth.html
providers:
- # - { name: 'google_oauth2',
+ # - { name: 'google_oauth2',
# label: 'Google',
- # app_id: 'YOUR_APP_ID',
+ # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
# args: { access_type: 'offline', approval_prompt: '' } }
- # - { name: 'twitter',
- # app_id: 'YOUR_APP_ID',
+ # - { name: 'twitter',
+ # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' }
- # - { name: 'github',
+ # - { name: 'github',
# label: 'GitHub',
- # app_id: 'YOUR_APP_ID',
+ # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
# args: { scope: 'user:email' } }
- # - { name: 'gitlab',
+ # - { name: 'gitlab',
# label: 'GitLab.com',
- # app_id: 'YOUR_APP_ID',
+ # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET',
# args: { scope: 'api' } }
- # - { name: 'bitbucket',
- # app_id: 'YOUR_APP_ID',
+ # - { name: 'bitbucket',
+ # app_id: 'YOUR_APP_ID',
# app_secret: 'YOUR_APP_SECRET' }
- # - { name: 'saml',
+ # - { name: 'saml',
# label: 'Our SAML Provider',
# args: {
# assertion_consumer_service_url: 'https://gitlab.example.com/users/auth/saml/callback',
@@ -246,12 +282,17 @@ production: &base
# issuer: 'https://gitlab.example.com',
# name_identifier_format: 'urn:oasis:names:tc:SAML:2.0:nameid-format:transient'
# } }
+ # - { name: 'crowd',
+ # args: {
+ # crowd_server_url: 'CROWD SERVER URL',
+ # application_name: 'YOUR_APP_NAME',
+ # application_password: 'YOUR_APP_PASSWORD' } }
#
- # 3. Advanced settings
+ # 4. Advanced settings
# ==========================
# GitLab Satellites
@@ -265,6 +306,7 @@ production: &base
path: "tmp/backups" # Relative paths are relative to Rails.root (default: tmp/backups/)
# archive_permissions: 0640 # Permissions for the resulting backup.tar file (default: 0600)
# keep_time: 604800 # default: 0 (forever) (in seconds)
+ # pg_schema: public # default: nil, it means that all schemas will be backed up
# upload:
# # Fog storage connection settings, see http://fog.io/storage/ .
# connection:
@@ -277,6 +319,8 @@ production: &base
# # Use multipart uploads when file size reaches 100MB, see
# # http://docs.aws.amazon.com/AmazonS3/latest/dev/uploadobjusingmpu.html
# multipart_chunk_size: 104857600
+ # # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+ # # encryption: 'AES256'
## GitLab Shell settings
gitlab_shell:
@@ -310,7 +354,7 @@ production: &base
timeout: 10
#
- # 4. Extra customization
+ # 5. Extra customization
# ==========================
extra:
@@ -380,7 +424,6 @@ test:
user_filter: ''
group_base: 'ou=groups,dc=example,dc=com'
admin_group: ''
- sync_ssh_keys: false
staging:
<<: *base
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index c47e5dab27c..4c78bd6e2fa 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -18,7 +18,19 @@ class Settings < Settingslogic
host.start_with?('www.') ? host[4..-1] : host
end
- private
+ def build_gitlab_ci_url
+ if gitlab_on_standard_port?
+ custom_port = nil
+ else
+ custom_port = ":#{gitlab.port}"
+ end
+ [ gitlab.protocol,
+ "://",
+ gitlab.host,
+ custom_port,
+ gitlab.relative_url_root
+ ].join('')
+ end
def build_gitlab_shell_ssh_path_prefix
if gitlab_shell.ssh_port != 22
@@ -97,6 +109,7 @@ if Settings.ldap['enabled'] || Rails.env.test?
server['block_auto_created_users'] = false if server['block_auto_created_users'].nil?
server['allow_username_or_email_login'] = false if server['allow_username_or_email_login'].nil?
server['active_directory'] = true if server['active_directory'].nil?
+ server['attributes'] = {} if server['attributes'].nil?
server['provider_name'] ||= "ldap#{key}".downcase
server['provider_class'] = OmniAuth::Utils.camelize(server['provider_name'])
end
@@ -158,13 +171,23 @@ Settings.gitlab.default_projects_features['snippets'] = false if Settings.
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['repository_downloads_path'] = File.absolute_path(Settings.gitlab['repository_downloads_path'] || 'tmp/repositories', Rails.root)
Settings.gitlab['restricted_signup_domains'] ||= []
-Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','git']
+Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
+
+
+#
+# CI
+#
+Settings['gitlab_ci'] ||= Settingslogic.new({})
+Settings.gitlab_ci['all_broken_builds'] = true if Settings.gitlab_ci['all_broken_builds'].nil?
+Settings.gitlab_ci['add_pusher'] = false if Settings.gitlab_ci['add_pusher'].nil?
+Settings.gitlab_ci['url'] ||= Settings.send(:build_gitlab_ci_url)
+Settings.gitlab_ci['builds_path'] = File.expand_path(Settings.gitlab_ci['builds_path'] || "builds/", Rails.root)
#
# Reply by email
#
-Settings['reply_by_email'] ||= Settingslogic.new({})
-Settings.reply_by_email['enabled'] = false if Settings.reply_by_email['enabled'].nil?
+Settings['incoming_email'] ||= Settingslogic.new({})
+Settings.incoming_email['enabled'] = false if Settings.incoming_email['enabled'].nil?
#
# Gravatar
@@ -196,6 +219,7 @@ Settings.gitlab_shell['ssh_path_prefix'] ||= Settings.send(:build_gitlab_shell_s
#
Settings['backup'] ||= Settingslogic.new({})
Settings.backup['keep_time'] ||= 0
+Settings.backup['pg_schema'] = nil
Settings.backup['path'] = File.expand_path(Settings.backup['path'] || "tmp/backups/", Rails.root)
Settings.backup['archive_permissions'] ||= 0600
Settings.backup['upload'] ||= Settingslogic.new({ 'remote_directory' => nil, 'connection' => nil })
@@ -204,6 +228,7 @@ if Settings.backup['upload']['connection']
Settings.backup['upload']['connection'] = Hash[Settings.backup['upload']['connection'].map { |k, v| [k.to_sym, v] }]
end
Settings.backup['upload']['multipart_chunk_size'] ||= 104857600
+Settings.backup['upload']['encryption'] ||= nil
#
# Git
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..cac8edb32bf
--- /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
+ Settings
+ 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
index 8fd27b1d88e..f9f88f95db9 100644
--- a/config/initializers/8_default_url_options.rb
+++ b/config/initializers/default_url_options.rb
@@ -8,4 +8,4 @@ unless Gitlab.config.gitlab_on_standard_port?
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/devise.rb b/config/initializers/devise.rb
index 091548348b1..29506970af2 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -148,6 +148,10 @@ Devise.setup do |config|
# When someone else invites you to GitLab this time is also used so it should be pretty long.
config.reset_password_within = 2.days
+ # When set to false, does not sign a user in automatically after their password is
+ # reset. Defaults to true, so a user is signed in automatically after a reset.
+ config.sign_in_after_reset_password = false
+
# ==> Configuration for :encryptable
# Allow you to use another encryption algorithm besides bcrypt (default). You can use
# :sha1, :sha512 or encryptors from others authentication tools as :clearance_sha1,
@@ -238,7 +242,7 @@ Devise.setup do |config|
provider_arguments.concat provider['args']
when Hash
# A Hash from the configuration will be passed as is.
- provider_arguments << provider['args']
+ provider_arguments << provider['args'].symbolize_keys
end
config.omniauth provider['name'].to_sym, *provider_arguments
diff --git a/config/initializers/7_omniauth.rb b/config/initializers/omniauth.rb
index 70ed10e8275..70ed10e8275 100644
--- a/config/initializers/7_omniauth.rb
+++ b/config/initializers/omniauth.rb
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
index 1d958904e8f..7710eeac453 100644
--- a/config/initializers/6_rack_profiler.rb
+++ b/config/initializers/rack_profiler.rb
@@ -2,7 +2,7 @@ if Rails.env.development?
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 @@ end
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..04ed9e90df5 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -2,7 +2,12 @@
require 'gitlab/current_settings'
include Gitlab::CurrentSettings
-Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay
+
+# allow it to fail: it may to do so when create_from_defaults is executed before migrations are actually done
+begin
+ Settings.gitlab['session_expire_delay'] = current_application_settings.session_expire_delay
+rescue
+end
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
@@ -11,5 +16,5 @@ Gitlab::Application.config.session_store(
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
index e856499732e..e856499732e 100644
--- a/config/initializers/4_sidekiq.rb
+++ b/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..22070e37f07 100644
--- a/config/locales/devise.en.yml
+++ b/config/locales/devise.en.yml
@@ -1,60 +1,63 @@
-# Additional translations at http://github.com/plataformatec/devise/wiki/I18n
+# Additional translations at https://github.com/plataformatec/devise/wiki/I18n
en:
+ devise:
+ confirmations:
+ confirmed: "Your email address has been successfully confirmed."
+ send_instructions: "You will receive an email with instructions for how to confirm your email address in a few minutes."
+ send_paranoid_instructions: "If your email address exists in our database, you will receive an email with instructions for how to confirm your email address in a few minutes."
+ failure:
+ already_authenticated: "You are already signed in."
+ inactive: "Your account is not activated yet."
+ invalid: "Invalid %{authentication_keys} or password."
+ locked: "Your account is locked."
+ last_attempt: "You have one more attempt before your account is locked."
+ not_found_in_database: "Invalid %{authentication_keys} or password."
+ timeout: "Your session expired. Please sign in again to continue."
+ unauthenticated: "You need to sign in or sign up before continuing."
+ unconfirmed: "You have to confirm your email address before continuing."
+ mailer:
+ confirmation_instructions:
+ subject: "Confirmation instructions"
+ reset_password_instructions:
+ subject: "Reset password instructions"
+ unlock_instructions:
+ subject: "Unlock instructions"
+ password_change:
+ subject: "Password Changed"
+ omniauth_callbacks:
+ failure: "Could not authenticate you from %{kind} because \"%{reason}\"."
+ success: "Successfully authenticated from %{kind} account."
+ passwords:
+ 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."
+ recently_reset: "Instructions about how to reset your password have already been sent recently. Please wait a few minutes to try again."
+ send_instructions: "You will receive an email with instructions on how to reset your password in a few minutes."
+ 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."
+ updated: "Your password has been changed successfully. You are now signed in."
+ updated_not_active: "Your password has been changed successfully."
+ registrations:
+ destroyed: "Bye! Your account has been successfully cancelled. We hope to see you again soon."
+ signed_up: "Welcome! You have signed up successfully."
+ signed_up_but_inactive: "You have signed up successfully. However, we could not sign you in because your account is not yet activated."
+ signed_up_but_locked: "You have signed up successfully. However, we could not sign you in because your account is locked."
+ signed_up_but_unconfirmed: "A message with a confirmation link has been sent to your email address. Please follow the link to activate your account."
+ update_needs_confirmation: "You updated your account successfully, but we need to verify your new email address. Please check your email and follow the confirm link to confirm your new email address."
+ updated: "Your account has been updated successfully."
+ sessions:
+ signed_in: "Signed in successfully."
+ signed_out: "Signed out successfully."
+ already_signed_out: "Signed out successfully."
+ unlocks:
+ send_instructions: "You will receive an email with instructions for how to unlock your account in a few minutes."
+ send_paranoid_instructions: "If your account exists, you will receive an email with instructions for how to unlock it in a few minutes."
+ unlocked: "Your account has been unlocked successfully. Please sign in to continue."
errors:
messages:
+ already_confirmed: "was already confirmed, please try signing in"
+ confirmation_period_expired: "needs to be confirmed within %{period}, please request a new one"
expired: "has expired, please request a new one"
not_found: "not found"
- already_confirmed: "was already confirmed, please try signing in"
not_locked: "was not locked"
not_saved:
one: "1 error prohibited this %{resource} from being saved:"
other: "%{count} errors prohibited this %{resource} from being saved:"
-
- devise:
- failure:
- already_authenticated: 'You are already signed in.'
- unauthenticated: 'You need to sign in before continuing.'
- unconfirmed: 'You have to confirm your account before continuing.'
- locked: 'Your account is locked.'
- not_found_in_database: 'Invalid email or password.'
- invalid: 'Invalid email or password.'
- invalid_token: 'Invalid authentication token.'
- timeout: 'Your session expired, please sign in again to continue.'
- inactive: 'Your account was not activated yet.'
- sessions:
- signed_in: ''
- signed_out: ''
- users_sessions:
- user:
- signed_in: 'Signed in successfully.'
- passwords:
- 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"
- 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.'
- confirmed: 'Your account was successfully confirmed. You are now signed in.'
- registrations:
- signed_up: 'Welcome! You have signed up successfully.'
- updated: 'You updated your account successfully.'
- destroyed: 'Bye! Your account was successfully cancelled. We hope to see you again soon.'
- signed_up_but_unconfirmed: 'A message with a confirmation link has been sent to your email address. Please open the link to activate your account.'
- signed_up_but_inactive: 'You have signed up successfully. However, we could not sign you in because your account is not yet activated.'
- signed_up_but_locked: 'You have signed up successfully. However, we could not sign you in because your account is locked.'
- unlocks:
- send_instructions: 'You will receive an email with instructions about how to unlock your account in a few minutes.'
- unlocked: 'Your account was successfully unlocked. You are now signed in.'
- send_paranoid_instructions: 'If your account exists, you will receive an email with instructions about how to unlock it in a few minutes.'
- omniauth_callbacks:
- success: 'Successfully authorized from %{kind} account.'
- failure: 'Could not authorize you from %{kind} because "%{reason}".'
- mailer:
- confirmation_instructions:
- subject: 'Confirmation instructions'
- reset_password_instructions:
- subject: 'Reset password instructions'
- unlock_instructions:
- subject: 'Unlock Instructions' \ No newline at end of file
diff --git a/config/mail_room.yml.example b/config/mail_room.yml.example
index dd8edfc42eb..82e1a42058e 100644
--- a/config/mail_room.yml.example
+++ b/config/mail_room.yml.example
@@ -6,6 +6,8 @@
# :port: 993
# # Whether the IMAP server uses SSL
# :ssl: true
+ # # Whether the IMAP server uses StartTLS
+ # :start_tls: false
# # Email account username. Usually the full email address.
# :email: "replies@gitlab.example.com"
# # Email account password
diff --git a/config/routes.rb b/config/routes.rb
index 920ece518ea..ccce40589e7 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,6 +2,80 @@ require 'sidekiq/web'
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]
+
+ resources :projects do
+ collection do
+ post :add
+ get :disabled
+ end
+
+ member do
+ get :status, to: 'projects#badge'
+ get :integration
+ post :toggle_shared_runners
+ get :dumped_yaml
+ end
+
+ resources :services, only: [:index, :edit, :update] do
+ member do
+ get :test
+ end
+ end
+
+ resources :commits, only: [] do
+ member do
+ get :status
+ get :cancel
+ end
+ end
+
+ resources :builds, only: [] do
+ member do
+ get :cancel
+ get :status
+ post :retry
+ end
+ end
+
+ resources :runner_projects, only: [:create, :destroy]
+
+ resources :events, only: [:index]
+ 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',
@@ -30,7 +104,7 @@ Gitlab::Application.routes.draw do
end
# Enable Grack support
- mount Grack::Auth.new({}), at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post]
+ mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post]
# Help
get 'help' => 'help#index'
@@ -99,6 +173,15 @@ Gitlab::Application.routes.draw do
get :new_user_map, path: :user_map
post :create_user_map, path: :user_map
end
+
+ resource :fogbugz, only: [:create, :new], controller: :fogbugz do
+ get :status
+ post :callback
+ get :jobs
+
+ get :new_user_map, path: :user_map
+ post :create_user_map, path: :user_map
+ end
end
#
@@ -134,6 +217,7 @@ Gitlab::Application.routes.draw do
end
resources :groups, only: [:index]
+ resources :snippets, only: [:index]
root to: 'projects#trending'
end
@@ -158,6 +242,7 @@ Gitlab::Application.routes.draw do
put :unblock
put :unlock
put :confirm
+ post :login_as
patch :disable_two_factor
delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
end
@@ -201,6 +286,8 @@ Gitlab::Application.routes.draw do
resources :services
end
+ resources :labels
+
root to: 'dashboard#index'
end
@@ -252,24 +339,25 @@ Gitlab::Application.routes.draw do
#
# Dashboard Area
#
- resource :dashboard, controller: 'dashboard', only: [:show] do
- member do
- get :issues
- get :merge_requests
- get :activity
- end
+ resource :dashboard, controller: 'dashboard', only: [] do
+ get :issues
+ get :merge_requests
+ get :activity
scope module: :dashboard do
resources :milestones, only: [:index, :show]
resources :groups, only: [:index]
+ resources :snippets, only: [:index]
- resources :projects, only: [] do
+ resources :projects, only: [:index] do
collection do
get :starred
end
end
end
+
+ root to: "dashboard/projects#index"
end
#
@@ -293,7 +381,7 @@ Gitlab::Application.routes.draw do
end
end
- resources :projects, constraints: { id: /[^\/]+/ }, only: [:new, :create]
+ resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks, registrations: :registrations , passwords: :passwords, sessions: :sessions, confirmations: :confirmations }
@@ -301,7 +389,7 @@ Gitlab::Application.routes.draw do
get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
end
- root to: "root#show"
+ root to: "root#index"
#
# Project Area
@@ -345,6 +433,16 @@ Gitlab::Application.routes.draw do
to: 'blob#destroy',
constraints: { id: /.+/, format: false }
)
+ put(
+ '/blob/*id',
+ to: 'blob#update',
+ constraints: { id: /.+/, format: false }
+ )
+ post(
+ '/blob/*id',
+ to: 'blob#create',
+ constraints: { id: /.+/, format: false }
+ )
end
scope do
@@ -385,7 +483,10 @@ Gitlab::Application.routes.draw do
resource :avatar, only: [:show, :destroy]
resources :commit, only: [:show], constraints: { id: /[[:alnum:]]{6,40}/ } do
- get :branches, on: :member
+ member do
+ get :branches
+ get :ci
+ end
end
resources :compare, only: [:index, :create]
@@ -394,6 +495,7 @@ Gitlab::Application.routes.draw do
resources :graphs, only: [:show], constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ } do
member do
get :commits
+ get :ci
end
end
@@ -406,16 +508,20 @@ Gitlab::Application.routes.draw do
end
end
- resources :wikis, only: [:show, :edit, :destroy, :create], constraints: { id: /[a-zA-Z.0-9_\-\/]+/ } do
- collection do
- get :pages
- put ':id' => 'wikis#update'
- get :git_access
- end
+ WIKI_SLUG_ID = { id: /[a-zA-Z.0-9_\-\/]+/ } unless defined? WIKI_SLUG_ID
- member do
- get 'history'
- end
+ scope do
+ # Order matters to give priority to these matches
+ get '/wikis/git_access', to: 'wikis#git_access'
+ get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
+ post '/wikis', to: 'wikis#create'
+
+ get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
+ get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
+
+ get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
+ delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
+ put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
end
resource :repository, only: [:show, :create] do
@@ -475,6 +581,16 @@ Gitlab::Application.routes.draw do
resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :tags, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
resources :protected_branches, only: [:index, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resource :variables, only: [:show, :update]
+ resources :triggers, only: [:index, :create, :destroy]
+ resource :ci_settings, only: [:edit, :update, :destroy]
+ resources :ci_web_hooks, only: [:index, :create, :destroy] do
+ member do
+ get :test
+ end
+ end
+
+ resources :builds, only: [:show]
resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
member do
@@ -530,8 +646,14 @@ Gitlab::Application.routes.draw do
get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
end
end
- end
+ resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+ member do
+ get :resume
+ get :pause
+ end
+ end
+ end
end
end
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.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/fixtures/production/001_admin.rb b/db/fixtures/production/001_admin.rb
index 1c8740f6ba9..b0c0b6450f6 100644
--- a/db/fixtures/production/001_admin.rb
+++ b/db/fixtures/production/001_admin.rb
@@ -19,7 +19,7 @@ admin = User.create(
admin.projects_limit = 10000
admin.admin = true
admin.save!
-admin.confirm!
+admin.confirm
if admin.valid?
puts %Q[
diff --git a/db/migrate/20150817163600_deduplicate_user_identities.rb b/db/migrate/20150817163600_deduplicate_user_identities.rb
index fab669c2905..fceffc48018 100644
--- a/db/migrate/20150817163600_deduplicate_user_identities.rb
+++ b/db/migrate/20150817163600_deduplicate_user_identities.rb
@@ -1,7 +1,7 @@
class DeduplicateUserIdentities < ActiveRecord::Migration
def change
execute 'DROP TABLE IF EXISTS tt_migration_DeduplicateUserIdentities;'
- execute 'CREATE TEMPORARY TABLE tt_migration_DeduplicateUserIdentities AS SELECT id,provider,user_id FROM identities;'
+ execute 'CREATE TABLE tt_migration_DeduplicateUserIdentities AS SELECT id,provider,user_id FROM identities;'
execute 'DELETE FROM identities WHERE id NOT IN ( SELECT MIN(id) FROM tt_migration_DeduplicateUserIdentities GROUP BY user_id, provider);'
execute 'DROP TABLE IF EXISTS tt_migration_DeduplicateUserIdentities;'
end
diff --git a/db/migrate/20150826001931_add_ci_tables.rb b/db/migrate/20150826001931_add_ci_tables.rb
new file mode 100644
index 00000000000..c4f51363e57
--- /dev/null
+++ b/db/migrate/20150826001931_add_ci_tables.rb
@@ -0,0 +1,190 @@
+class AddCiTables < ActiveRecord::Migration
+ def change
+ create_table "ci_application_settings", force: true do |t|
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_builds", force: true do |t|
+ t.integer "project_id"
+ t.string "status"
+ t.datetime "finished_at"
+ t.text "trace"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.datetime "started_at"
+ t.integer "runner_id"
+ t.float "coverage"
+ t.integer "commit_id"
+ t.text "commands"
+ t.integer "job_id"
+ t.string "name"
+ t.boolean "deploy", default: false
+ t.text "options"
+ t.boolean "allow_failure", default: false, null: false
+ t.string "stage"
+ t.integer "trigger_request_id"
+ end
+
+ add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
+ add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
+ add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
+ add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+
+ create_table "ci_commits", force: true do |t|
+ t.integer "project_id"
+ t.string "ref"
+ t.string "sha"
+ t.string "before_sha"
+ t.text "push_data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "tag", default: false
+ t.text "yaml_errors"
+ t.datetime "committed_at"
+ end
+
+ add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
+ add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
+ add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
+ add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
+
+ create_table "ci_events", force: true do |t|
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree
+ add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
+ add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
+
+ create_table "ci_jobs", force: true do |t|
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
+ t.datetime "deleted_at"
+ end
+
+ add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
+ add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
+
+ create_table "ci_projects", force: true do |t|
+ t.string "name", null: false
+ t.integer "timeout", default: 3600, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
+ end
+
+ create_table "ci_runner_projects", force: true do |t|
+ t.integer "runner_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
+ add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
+
+ create_table "ci_runners", force: true do |t|
+ t.string "token"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "description"
+ t.datetime "contacted_at"
+ t.boolean "active", default: true, null: false
+ t.boolean "is_shared", default: false
+ t.string "name"
+ t.string "version"
+ t.string "revision"
+ t.string "platform"
+ t.string "architecture"
+ end
+
+ create_table "ci_services", force: true do |t|
+ t.string "type"
+ t.string "title"
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "active", default: false, null: false
+ t.text "properties"
+ end
+
+ add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
+
+ create_table "ci_sessions", force: true do |t|
+ t.string "session_id", null: false
+ t.text "data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
+ add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
+
+ create_table "ci_trigger_requests", force: true do |t|
+ t.integer "trigger_id", null: false
+ t.text "variables"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "commit_id"
+ end
+
+ create_table "ci_triggers", force: true do |t|
+ t.string "token"
+ t.integer "project_id", null: false
+ t.datetime "deleted_at"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
+
+ create_table "ci_variables", force: true do |t|
+ t.integer "project_id", null: false
+ t.string "key"
+ t.text "value"
+ t.text "encrypted_value"
+ t.string "encrypted_value_salt"
+ t.string "encrypted_value_iv"
+ end
+
+ add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
+
+ create_table "ci_web_hooks", force: true do |t|
+ t.string "url", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+ end
+end
diff --git a/db/migrate/20150902001023_add_template_to_label.rb b/db/migrate/20150902001023_add_template_to_label.rb
new file mode 100644
index 00000000000..bd381a97b69
--- /dev/null
+++ b/db/migrate/20150902001023_add_template_to_label.rb
@@ -0,0 +1,5 @@
+class AddTemplateToLabel < ActiveRecord::Migration
+ def change
+ add_column :labels, :template, :boolean, default: false
+ end
+end \ No newline at end of file
diff --git a/db/migrate/20150914215247_add_ci_tags.rb b/db/migrate/20150914215247_add_ci_tags.rb
new file mode 100644
index 00000000000..df3390e8a82
--- /dev/null
+++ b/db/migrate/20150914215247_add_ci_tags.rb
@@ -0,0 +1,23 @@
+class AddCiTags < ActiveRecord::Migration
+ def change
+ create_table "ci_taggings", force: true do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
+ add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
+
+ create_table "ci_tags", force: true do |t|
+ t.string "name"
+ t.integer "taggings_count", default: 0
+ end
+
+ add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
+ end
+end
diff --git a/db/migrate/20150915001905_enable_ssl_verification_by_default.rb b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb
new file mode 100644
index 00000000000..6e924262a13
--- /dev/null
+++ b/db/migrate/20150915001905_enable_ssl_verification_by_default.rb
@@ -0,0 +1,5 @@
+class EnableSslVerificationByDefault < ActiveRecord::Migration
+ def change
+ change_column :web_hooks, :enable_ssl_verification, :boolean, default: true
+ end
+end
diff --git a/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb
new file mode 100644
index 00000000000..90ce6c2db3d
--- /dev/null
+++ b/db/migrate/20150916000405_enable_ssl_verification_for_web_hooks.rb
@@ -0,0 +1,8 @@
+class EnableSslVerificationForWebHooks < ActiveRecord::Migration
+ def up
+ execute("UPDATE web_hooks SET enable_ssl_verification = true")
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb
new file mode 100644
index 00000000000..37a27f11935
--- /dev/null
+++ b/db/migrate/20150916114643_add_help_page_text_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddHelpPageTextToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :help_page_text, :text
+ end
+end
diff --git a/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb
new file mode 100644
index 00000000000..78d9e5f61a1
--- /dev/null
+++ b/db/migrate/20150916145038_add_index_for_committed_at_and_id.rb
@@ -0,0 +1,5 @@
+class AddIndexForCommittedAtAndId < ActiveRecord::Migration
+ def change
+ add_index :ci_commits, [:project_id, :committed_at, :id]
+ end
+end
diff --git a/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..6cf668a170e
--- /dev/null
+++ b/db/migrate/20150918084513_add_ci_enabled_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddCiEnabledToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :ci_enabled, :boolean, null: false, default: true
+ end
+end
diff --git a/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb
new file mode 100644
index 00000000000..0aad6fe5e6e
--- /dev/null
+++ b/db/migrate/20150918161719_remove_invalid_milestones_from_merge_requests.rb
@@ -0,0 +1,5 @@
+class RemoveInvalidMilestonesFromMergeRequests < ActiveRecord::Migration
+ def up
+ execute("UPDATE merge_requests SET milestone_id = NULL where milestone_id NOT IN (SELECT id FROM milestones)")
+ end
+end
diff --git a/db/migrate/20150920010715_add_consumed_timestep_to_users.rb b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb
new file mode 100644
index 00000000000..c8438b3f6aa
--- /dev/null
+++ b/db/migrate/20150920010715_add_consumed_timestep_to_users.rb
@@ -0,0 +1,5 @@
+class AddConsumedTimestepToUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :consumed_timestep, :integer
+ end
+end
diff --git a/db/migrate/20150920161119_add_line_code_to_sent_notification.rb b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb
new file mode 100644
index 00000000000..d9af4e71751
--- /dev/null
+++ b/db/migrate/20150920161119_add_line_code_to_sent_notification.rb
@@ -0,0 +1,5 @@
+class AddLineCodeToSentNotification < ActiveRecord::Migration
+ def change
+ add_column :sent_notifications, :line_code, :string
+ end
+end
diff --git a/db/migrate/20150924125150_add_project_id_to_ci_commit.rb b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb
new file mode 100644
index 00000000000..1a761fe0f86
--- /dev/null
+++ b/db/migrate/20150924125150_add_project_id_to_ci_commit.rb
@@ -0,0 +1,5 @@
+class AddProjectIdToCiCommit < ActiveRecord::Migration
+ def up
+ add_column :ci_commits, :gl_project_id, :integer
+ end
+end
diff --git a/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb
new file mode 100644
index 00000000000..2be57b6062e
--- /dev/null
+++ b/db/migrate/20150924125436_migrate_project_id_for_ci_commits.rb
@@ -0,0 +1,6 @@
+class MigrateProjectIdForCiCommits < ActiveRecord::Migration
+ def up
+ subquery = 'SELECT gitlab_id FROM ci_projects WHERE ci_projects.id = ci_commits.project_id'
+ execute("UPDATE ci_commits SET gl_project_id=(#{subquery}) WHERE gl_project_id IS NULL")
+ end
+end
diff --git a/db/migrate/20150930001110_merge_request_error_field.rb b/db/migrate/20150930001110_merge_request_error_field.rb
new file mode 100644
index 00000000000..c2ee498ef3f
--- /dev/null
+++ b/db/migrate/20150930001110_merge_request_error_field.rb
@@ -0,0 +1,5 @@
+class MergeRequestErrorField < ActiveRecord::Migration
+ def up
+ add_column :merge_requests, :merge_error, :string
+ end
+end
diff --git a/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb
new file mode 100644
index 00000000000..8d47dac6441
--- /dev/null
+++ b/db/migrate/20150930095736_add_null_to_name_for_ci_projects.rb
@@ -0,0 +1,9 @@
+class AddNullToNameForCiProjects < ActiveRecord::Migration
+ def up
+ change_column_null :ci_projects, :name, true
+ end
+
+ def down
+ change_column_null :ci_projects, :name, false
+ end
+end
diff --git a/db/migrate/20151002112914_add_stage_idx_to_builds.rb b/db/migrate/20151002112914_add_stage_idx_to_builds.rb
new file mode 100644
index 00000000000..68a745ffef4
--- /dev/null
+++ b/db/migrate/20151002112914_add_stage_idx_to_builds.rb
@@ -0,0 +1,5 @@
+class AddStageIdxToBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :stage_idx, :integer
+ end
+end
diff --git a/db/migrate/20151002121400_add_index_for_builds.rb b/db/migrate/20151002121400_add_index_for_builds.rb
new file mode 100644
index 00000000000..4ffc1363910
--- /dev/null
+++ b/db/migrate/20151002121400_add_index_for_builds.rb
@@ -0,0 +1,5 @@
+class AddIndexForBuilds < ActiveRecord::Migration
+ def up
+ add_index :ci_builds, [:commit_id, :stage_idx, :created_at]
+ end
+end
diff --git a/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb
new file mode 100644
index 00000000000..e3d2ac1cea5
--- /dev/null
+++ b/db/migrate/20151002122929_add_ref_and_tag_to_builds.rb
@@ -0,0 +1,6 @@
+class AddRefAndTagToBuilds < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :tag, :boolean
+ add_column :ci_builds, :ref, :string
+ end
+end
diff --git a/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb
new file mode 100644
index 00000000000..01d7b3f6773
--- /dev/null
+++ b/db/migrate/20151002122943_migrate_ref_and_tag_to_build.rb
@@ -0,0 +1,6 @@
+class MigrateRefAndTagToBuild < ActiveRecord::Migration
+ def change
+ execute('UPDATE ci_builds SET ref=(SELECT ref FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE ref IS NULL')
+ execute('UPDATE ci_builds SET tag=(SELECT tag FROM ci_commits WHERE ci_commits.id = ci_builds.commit_id) WHERE tag IS NULL')
+ end
+end
diff --git a/db/migrate/20151005075649_add_user_id_to_build.rb b/db/migrate/20151005075649_add_user_id_to_build.rb
new file mode 100644
index 00000000000..0f4b92b8b79
--- /dev/null
+++ b/db/migrate/20151005075649_add_user_id_to_build.rb
@@ -0,0 +1,5 @@
+class AddUserIdToBuild < ActiveRecord::Migration
+ def change
+ add_column :ci_builds, :user_id, :integer
+ end
+end
diff --git a/db/migrate/20151005150751_add_layout_option_for_users.rb b/db/migrate/20151005150751_add_layout_option_for_users.rb
new file mode 100644
index 00000000000..ead9b1f8977
--- /dev/null
+++ b/db/migrate/20151005150751_add_layout_option_for_users.rb
@@ -0,0 +1,5 @@
+class AddLayoutOptionForUsers < ActiveRecord::Migration
+ def change
+ add_column :users, :layout, :integer, default: 0
+ end
+end \ No newline at end of file
diff --git a/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb
new file mode 100644
index 00000000000..be6aa810bb5
--- /dev/null
+++ b/db/migrate/20151005162154_remove_ci_enabled_from_application_settings.rb
@@ -0,0 +1,5 @@
+class RemoveCiEnabledFromApplicationSettings < ActiveRecord::Migration
+ def change
+ remove_column :application_settings, :ci_enabled, :boolean, null: false, default: true
+ end
+end
diff --git a/db/migrate/limits_to_mysql.rb b/db/migrate/limits_to_mysql.rb
index 2b7afae6d7c..73605d4c5e3 100644
--- a/db/migrate/limits_to_mysql.rb
+++ b/db/migrate/limits_to_mysql.rb
@@ -6,5 +6,9 @@ class LimitsToMysql < ActiveRecord::Migration
change_column :merge_request_diffs, :st_diffs, :text, limit: 2147483647
change_column :snippets, :content, :text, limit: 2147483647
change_column :notes, :st_diff, :text, limit: 2147483647
+
+ # CI
+ change_column :ci_builds, :trace, :text, limit: 1073741823
+ change_column :ci_commits, :push_data, :text, limit: 16777215
end
end
diff --git a/db/schema.rb b/db/schema.rb
index 36568dd4edd..93202f16111 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20150824002011) do
+ActiveRecord::Schema.define(version: 20151005162154) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -45,6 +45,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do
t.string "after_sign_out_path"
t.integer "session_expire_delay", default: 10080, null: false
t.text "import_sources"
+ t.text "help_page_text"
end
create_table "audit_events", force: true do |t|
@@ -72,6 +73,220 @@ ActiveRecord::Schema.define(version: 20150824002011) do
t.string "font"
end
+ create_table "ci_application_settings", force: true do |t|
+ t.boolean "all_broken_builds"
+ t.boolean "add_pusher"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ create_table "ci_builds", force: true do |t|
+ t.integer "project_id"
+ t.string "status"
+ t.datetime "finished_at"
+ t.text "trace"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.datetime "started_at"
+ t.integer "runner_id"
+ t.float "coverage"
+ t.integer "commit_id"
+ t.text "commands"
+ t.integer "job_id"
+ t.string "name"
+ t.boolean "deploy", default: false
+ t.text "options"
+ t.boolean "allow_failure", default: false, null: false
+ t.string "stage"
+ t.integer "trigger_request_id"
+ t.integer "stage_idx"
+ t.boolean "tag"
+ t.string "ref"
+ t.integer "user_id"
+ end
+
+ add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
+ add_index "ci_builds", ["commit_id"], name: "index_ci_builds_on_commit_id", using: :btree
+ add_index "ci_builds", ["project_id", "commit_id"], name: "index_ci_builds_on_project_id_and_commit_id", using: :btree
+ add_index "ci_builds", ["project_id"], name: "index_ci_builds_on_project_id", using: :btree
+ add_index "ci_builds", ["runner_id"], name: "index_ci_builds_on_runner_id", using: :btree
+
+ create_table "ci_commits", force: true do |t|
+ t.integer "project_id"
+ t.string "ref"
+ t.string "sha"
+ t.string "before_sha"
+ t.text "push_data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "tag", default: false
+ t.text "yaml_errors"
+ t.datetime "committed_at"
+ t.integer "gl_project_id"
+ end
+
+ add_index "ci_commits", ["project_id", "committed_at", "id"], name: "index_ci_commits_on_project_id_and_committed_at_and_id", using: :btree
+ add_index "ci_commits", ["project_id", "committed_at"], name: "index_ci_commits_on_project_id_and_committed_at", using: :btree
+ add_index "ci_commits", ["project_id", "sha"], name: "index_ci_commits_on_project_id_and_sha", using: :btree
+ add_index "ci_commits", ["project_id"], name: "index_ci_commits_on_project_id", using: :btree
+ add_index "ci_commits", ["sha"], name: "index_ci_commits_on_sha", using: :btree
+
+ create_table "ci_events", force: true do |t|
+ t.integer "project_id"
+ t.integer "user_id"
+ t.integer "is_admin"
+ t.text "description"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_events", ["created_at"], name: "index_ci_events_on_created_at", using: :btree
+ add_index "ci_events", ["is_admin"], name: "index_ci_events_on_is_admin", using: :btree
+ add_index "ci_events", ["project_id"], name: "index_ci_events_on_project_id", using: :btree
+
+ create_table "ci_jobs", force: true do |t|
+ t.integer "project_id", null: false
+ t.text "commands"
+ t.boolean "active", default: true, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "name"
+ t.boolean "build_branches", default: true, null: false
+ t.boolean "build_tags", default: false, null: false
+ t.string "job_type", default: "parallel"
+ t.string "refs"
+ t.datetime "deleted_at"
+ end
+
+ add_index "ci_jobs", ["deleted_at"], name: "index_ci_jobs_on_deleted_at", using: :btree
+ add_index "ci_jobs", ["project_id"], name: "index_ci_jobs_on_project_id", using: :btree
+
+ create_table "ci_projects", force: true do |t|
+ t.string "name"
+ t.integer "timeout", default: 3600, null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "token"
+ t.string "default_ref"
+ t.string "path"
+ t.boolean "always_build", default: false, null: false
+ t.integer "polling_interval"
+ t.boolean "public", default: false, null: false
+ t.string "ssh_url_to_repo"
+ t.integer "gitlab_id"
+ t.boolean "allow_git_fetch", default: true, null: false
+ t.string "email_recipients", default: "", null: false
+ t.boolean "email_add_pusher", default: true, null: false
+ t.boolean "email_only_broken_builds", default: true, null: false
+ t.string "skip_refs"
+ t.string "coverage_regex"
+ t.boolean "shared_runners_enabled", default: false
+ t.text "generated_yaml_config"
+ end
+
+ create_table "ci_runner_projects", force: true do |t|
+ t.integer "runner_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_runner_projects", ["project_id"], name: "index_ci_runner_projects_on_project_id", using: :btree
+ add_index "ci_runner_projects", ["runner_id"], name: "index_ci_runner_projects_on_runner_id", using: :btree
+
+ create_table "ci_runners", force: true do |t|
+ t.string "token"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "description"
+ t.datetime "contacted_at"
+ t.boolean "active", default: true, null: false
+ t.boolean "is_shared", default: false
+ t.string "name"
+ t.string "version"
+ t.string "revision"
+ t.string "platform"
+ t.string "architecture"
+ end
+
+ create_table "ci_services", force: true do |t|
+ t.string "type"
+ t.string "title"
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.boolean "active", default: false, null: false
+ t.text "properties"
+ end
+
+ add_index "ci_services", ["project_id"], name: "index_ci_services_on_project_id", using: :btree
+
+ create_table "ci_sessions", force: true do |t|
+ t.string "session_id", null: false
+ t.text "data"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_sessions", ["session_id"], name: "index_ci_sessions_on_session_id", using: :btree
+ add_index "ci_sessions", ["updated_at"], name: "index_ci_sessions_on_updated_at", using: :btree
+
+ create_table "ci_taggings", force: true do |t|
+ t.integer "tag_id"
+ t.integer "taggable_id"
+ t.string "taggable_type"
+ t.integer "tagger_id"
+ t.string "tagger_type"
+ t.string "context", limit: 128
+ t.datetime "created_at"
+ end
+
+ add_index "ci_taggings", ["tag_id", "taggable_id", "taggable_type", "context", "tagger_id", "tagger_type"], name: "ci_taggings_idx", unique: true, using: :btree
+ add_index "ci_taggings", ["taggable_id", "taggable_type", "context"], name: "index_ci_taggings_on_taggable_id_and_taggable_type_and_context", using: :btree
+
+ create_table "ci_tags", force: true do |t|
+ t.string "name"
+ t.integer "taggings_count", default: 0
+ end
+
+ add_index "ci_tags", ["name"], name: "index_ci_tags_on_name", unique: true, using: :btree
+
+ create_table "ci_trigger_requests", force: true do |t|
+ t.integer "trigger_id", null: false
+ t.text "variables"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.integer "commit_id"
+ end
+
+ create_table "ci_triggers", force: true do |t|
+ t.string "token"
+ t.integer "project_id", null: false
+ t.datetime "deleted_at"
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "ci_triggers", ["deleted_at"], name: "index_ci_triggers_on_deleted_at", using: :btree
+
+ create_table "ci_variables", force: true do |t|
+ t.integer "project_id", null: false
+ t.string "key"
+ t.text "value"
+ t.text "encrypted_value"
+ t.string "encrypted_value_salt"
+ t.string "encrypted_value_iv"
+ end
+
+ add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
+
+ create_table "ci_web_hooks", force: true do |t|
+ t.string "url", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
create_table "deploy_keys_projects", force: true do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
@@ -186,6 +401,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
+ t.boolean "template", default: false
end
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
@@ -241,6 +457,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do
t.integer "position", default: 0
t.datetime "locked_at"
t.integer "updated_by_id"
+ t.string "merge_error"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -412,6 +629,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do
t.integer "recipient_id"
t.string "commit_id"
t.string "reply_key", null: false
+ t.string "line_code"
end
add_index "sent_notifications", ["reply_key"], name: "index_sent_notifications_on_reply_key", unique: true, using: :btree
@@ -538,6 +756,8 @@ ActiveRecord::Schema.define(version: 20150824002011) do
t.string "public_email", default: "", null: false
t.integer "dashboard", default: 0
t.integer "project_view", default: 0
+ t.integer "consumed_timestep"
+ t.integer "layout", default: 0
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -573,7 +793,7 @@ ActiveRecord::Schema.define(version: 20150824002011) do
t.boolean "merge_requests_events", default: false, null: false
t.boolean "tag_push_events", default: false
t.boolean "note_events", default: false, null: false
- t.boolean "enable_ssl_verification", default: false
+ t.boolean "enable_ssl_verification", default: true
end
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
diff --git a/doc/README.md b/doc/README.md
index 337c4e6a62d..a0ff856ebb6 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -15,6 +15,23 @@
- [Web hooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
+## CI Documentation
+
++ [Quick Start](ci/quick_start/README.md)
++ [Configuring project (.gitlab-ci.yml)](ci/yaml/README.md)
++ [Configuring runner](ci/runners/README.md)
++ [Configuring deployment](ci/deployment/README.md)
++ [Using Docker Images](ci/docker/using_docker_images.md)
++ [Using Docker Build](ci/docker/using_docker_build.md)
++ [Using Variables](ci/variables/README.md)
+
+### CI Examples
+
++ [Test and deploy Ruby applications to Heroku](ci/examples/test-and-deploy-ruby-application-to-heroku.md)
++ [Test and deploy Python applications to Heroku](ci/examples/test-and-deploy-python-application-to-heroku.md)
++ [Test Clojure applications](ci/examples/test-clojure-application.md)
++ Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
+
## Administrator documentation
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when web hooks aren't enough.
@@ -29,7 +46,13 @@
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
- [Welcome message](customization/welcome_message.md) Add a custom welcome message to the sign-in page.
-- [Reply by email](reply_by_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
+- [Reply by email](incoming_email/README.md) Allow users to comment on issues and merge requests by replying to notification emails.
+- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
+
+### Administrator documentation
+
++ [User permissions](permissions/permissions.md)
++ [API](api/README.md)
## Contributor documentation
diff --git a/doc/api/README.md b/doc/api/README.md
index f369c3fd978..6b8528de50c 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -21,6 +21,7 @@
- [Groups](groups.md)
- [Namespaces](namespaces.md)
- [Settings](settings.md)
+- [Keys](keys.md)
## Clients
diff --git a/doc/api/keys.md b/doc/api/keys.md
new file mode 100644
index 00000000000..faa6f212b43
--- /dev/null
+++ b/doc/api/keys.md
@@ -0,0 +1,46 @@
+# Keys
+
+## Get SSH key with user by ID of an SSH key
+
+Get SSH key with user by ID of an SSH key. Note only administrators can lookup SSH key with user by ID of an SSH key.
+
+```
+GET /keys/:id
+```
+
+Parameters:
+
+- `id` (required) - The ID of an SSH key
+
+```json
+{
+ "id": 1,
+ "title": "Sample key 25",
+ "key": "ssh-rsa AAAAB3NzaC1yc2EAAAABJQAAAIEAiPWx6WM4lhHNedGfBpPJNPpZ7yKu+dnn1SJejgt1256k6YjzGGphH2TUxwKzxcKDKKezwkpfnxPkSMkuEspGRt/aZZ9wa++Oi7Qkr8prgHc4soW6NUlfDzpvZK2H5E7eQaSeP3SAwGmQKUFHCddNaP0L+hM7zhFNzjFvpaMgJw0=",
+ "created_at": "2015-09-03T07:24:44.627Z",
+ "user": {
+ "name": "John Smith",
+ "username": "john_smith",
+ "id": 25,
+ "state": "active",
+ "avatar_url": "http://www.gravatar.com/avatar/cfa35b8cd2ec278026357769582fa563?s=40\u0026d=identicon",
+ "web_url": "http://localhost:3000/u/john_smith",
+ "created_at": "2015-09-03T07:24:01.670Z",
+ "is_admin": false,
+ "bio": null,
+ "skype": "",
+ "linkedin": "",
+ "twitter": "",
+ "website_url": "",
+ "email": "john@example.com",
+ "theme_id": 2,
+ "color_scheme_id": 1,
+ "projects_limit": 10,
+ "current_sign_in_at": null,
+ "identities": [],
+ "can_create_group": true,
+ "can_create_project": true,
+ "two_factor_enabled": false
+ }
+}
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 10533c73a31..96485857035 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -515,6 +515,8 @@ Parameters:
"push_events": "true",
"issues_events": "true",
"merge_requests_events": "true",
+ "note_events": "true",
+ "enable_ssl_verification": "true",
"created_at": "2012-10-12T17:04:47Z"
}
```
@@ -535,6 +537,8 @@ Parameters:
- `issues_events` - Trigger hook on issues events
- `merge_requests_events` - Trigger hook on merge_requests events
- `tag_push_events` - Trigger hook on push_tag events
+- `note_events` - Trigger hook on note events
+- `enable_ssl_verification` - Do SSL verification when triggering the hook
### Edit project hook
@@ -553,6 +557,8 @@ Parameters:
- `issues_events` - Trigger hook on issues events
- `merge_requests_events` - Trigger hook on merge_requests events
- `tag_push_events` - Trigger hook on push_tag events
+- `note_events` - Trigger hook on note events
+- `enable_ssl_verification` - Do SSL verification when triggering the hook
### Delete project hook
diff --git a/doc/api/services.md b/doc/api/services.md
index cbf767d1b25..7d45b2cf463 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -1,8 +1,384 @@
# Services
+## Asana
+
+Asana - Teamwork without email
+
+### Create/Edit Asana service
+
+Set Asana service for a project.
+
+> This service adds commit messages as comments to Asana tasks. Once enabled, commit messages are checked for Asana task URLs (for example, `https://app.asana.com/0/123456/987654`) or task IDs starting with # (for example, `#987654`). Every task ID found will get the commit comment added to it. You can also close a task with a message containing: `fix #123456`. You can find your Api Keys here: http://developer.asana.com/documentation/#api_keys
+
+```
+PUT /projects/:id/services/asana
+```
+
+Parameters:
+
+- `api_key` (**required**) - User API token. User must have access to task,all comments will be attributed to this user.
+- `restrict_to_branch` (optional) - Comma-separated list of branches which will beautomatically inspected. Leave blank to include all branches.
+
+### Delete Asana service
+
+Delete Asana service for a project.
+
+```
+DELETE /projects/:id/services/asana
+```
+
+### Get Asana service settings
+
+Get Asana service settings for a project.
+
+```
+GET /projects/:id/services/asana
+```
+
+## Assembla
+
+Project Management Software (Source Commits Endpoint)
+
+### Create/Edit Assembla service
+
+Set Assembla service for a project.
+
+```
+PUT /projects/:id/services/assembla
+```
+
+Parameters:
+
+- `token` (**required**)
+- `subdomain` (optional)
+
+### Delete Assembla service
+
+Delete Assembla service for a project.
+
+```
+DELETE /projects/:id/services/assembla
+```
+
+### Get Assembla service settings
+
+Get Assembla service settings for a project.
+
+```
+GET /projects/:id/services/assembla
+```
+
+## Atlassian Bamboo CI
+
+A continuous integration and build server
+
+### Create/Edit Atlassian Bamboo CI service
+
+Set Atlassian Bamboo CI service for a project.
+
+> You must set up automatic revision labeling and a repository trigger in Bamboo.
+
+```
+PUT /projects/:id/services/bamboo
+```
+
+Parameters:
+
+- `bamboo_url` (**required**) - Bamboo root URL like https://bamboo.example.com
+- `build_key` (**required**) - Bamboo build plan key like KEY
+- `username` (**required**) - A user with API access, if applicable
+- `password` (**required**)
+
+### Delete Atlassian Bamboo CI service
+
+Delete Atlassian Bamboo CI service for a project.
+
+```
+DELETE /projects/:id/services/bamboo
+```
+
+### Get Atlassian Bamboo CI service settings
+
+Get Atlassian Bamboo CI service settings for a project.
+
+```
+GET /projects/:id/services/bamboo
+```
+
+## Buildkite
+
+Continuous integration and deployments
+
+### Create/Edit Buildkite service
+
+Set Buildkite service for a project.
+
+```
+PUT /projects/:id/services/buildkite
+```
+
+Parameters:
+
+- `token` (**required**) - Buildkite project GitLab token
+- `project_url` (**required**) - https://buildkite.com/example/project
+- `enable_ssl_verification` (optional) - Enable SSL verification
+
+### Delete Buildkite service
+
+Delete Buildkite service for a project.
+
+```
+DELETE /projects/:id/services/buildkite
+```
+
+### Get Buildkite service settings
+
+Get Buildkite service settings for a project.
+
+```
+GET /projects/:id/services/buildkite
+```
+
+## Campfire
+
+Simple web-based real-time group chat
+
+### Create/Edit Campfire service
+
+Set Campfire service for a project.
+
+```
+PUT /projects/:id/services/campfire
+```
+
+Parameters:
+
+- `token` (**required**)
+- `subdomain` (optional)
+- `room` (optional)
+
+### Delete Campfire service
+
+Delete Campfire service for a project.
+
+```
+DELETE /projects/:id/services/campfire
+```
+
+### Get Campfire service settings
+
+Get Campfire service settings for a project.
+
+```
+GET /projects/:id/services/campfire
+```
+
+## Custom Issue Tracker
+
+Custom issue tracker
+
+### Create/Edit Custom Issue Tracker service
+
+Set Custom Issue Tracker service for a project.
+
+```
+PUT /projects/:id/services/custom-issue-tracker
+```
+
+Parameters:
+
+- `new_issue_url` (**required**) - New Issue url
+- `issues_url` (**required**) - Issue url
+- `project_url` (**required**) - Project url
+- `description` (optional) - Custom issue tracker
+- `title` (optional) - Custom Issue Tracker
+
+### Delete Custom Issue Tracker service
+
+Delete Custom Issue Tracker service for a project.
+
+```
+DELETE /projects/:id/services/custom-issue-tracker
+```
+
+### Get Custom Issue Tracker service settings
+
+Get Custom Issue Tracker service settings for a project.
+
+```
+GET /projects/:id/services/custom-issue-tracker
+```
+
+## Drone CI
+
+Drone is a Continuous Integration platform built on Docker, written in Go
+
+### Create/Edit Drone CI service
+
+Set Drone CI service for a project.
+
+```
+PUT /projects/:id/services/drone-ci
+```
+
+Parameters:
+
+- `token` (**required**) - Drone CI project specific token
+- `drone_url` (**required**) - http://drone.example.com
+- `enable_ssl_verification` (optional) - Enable SSL verification
+
+### Delete Drone CI service
+
+Delete Drone CI service for a project.
+
+```
+DELETE /projects/:id/services/drone-ci
+```
+
+### Get Drone CI service settings
+
+Get Drone CI service settings for a project.
+
+```
+GET /projects/:id/services/drone-ci
+```
+
+## Emails on push
+
+Email the commits and diff of each push to a list of recipients.
+
+### Create/Edit Emails on push service
+
+Set Emails on push service for a project.
+
+```
+PUT /projects/:id/services/emails-on-push
+```
+
+Parameters:
+
+- `recipients` (**required**) - Emails separated by whitespace
+- `disable_diffs` (optional) - Disable code diffs
+- `send_from_committer_email` (optional) - Send from committer
+
+### Delete Emails on push service
+
+Delete Emails on push service for a project.
+
+```
+DELETE /projects/:id/services/emails-on-push
+```
+
+### Get Emails on push service settings
+
+Get Emails on push service settings for a project.
+
+```
+GET /projects/:id/services/emails-on-push
+```
+
+## External Wiki
+
+Replaces the link to the internal wiki with a link to an external wiki.
+
+### Create/Edit External Wiki service
+
+Set External Wiki service for a project.
+
+```
+PUT /projects/:id/services/external-wiki
+```
+
+Parameters:
+
+- `external_wiki_url` (**required**) - The URL of the external Wiki
+
+### Delete External Wiki service
+
+Delete External Wiki service for a project.
+
+```
+DELETE /projects/:id/services/external-wiki
+```
+
+### Get External Wiki service settings
+
+Get External Wiki service settings for a project.
+
+```
+GET /projects/:id/services/external-wiki
+```
+
+## Flowdock
+
+Flowdock is a collaboration web app for technical teams.
+
+### Create/Edit Flowdock service
+
+Set Flowdock service for a project.
+
+```
+PUT /projects/:id/services/flowdock
+```
+
+Parameters:
+
+- `token` (**required**) - Flowdock Git source token
+
+### Delete Flowdock service
+
+Delete Flowdock service for a project.
+
+```
+DELETE /projects/:id/services/flowdock
+```
+
+### Get Flowdock service settings
+
+Get Flowdock service settings for a project.
+
+```
+GET /projects/:id/services/flowdock
+```
+
+## Gemnasium
+
+Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.
+
+### Create/Edit Gemnasium service
+
+Set Gemnasium service for a project.
+
+```
+PUT /projects/:id/services/gemnasium
+```
+
+Parameters:
+
+- `api_key` (**required**) - Your personal API KEY on gemnasium.com
+- `token` (**required**) - The project's slug on gemnasium.com
+
+### Delete Gemnasium service
+
+Delete Gemnasium service for a project.
+
+```
+DELETE /projects/:id/services/gemnasium
+```
+
+### Get Gemnasium service settings
+
+Get Gemnasium service settings for a project.
+
+```
+GET /projects/:id/services/gemnasium
+```
+
## GitLab CI
-### Edit GitLab CI service
+Continuous integration server from GitLab
+
+### Create/Edit GitLab CI service
Set GitLab CI service for a project.
@@ -12,30 +388,46 @@ PUT /projects/:id/services/gitlab-ci
Parameters:
-- `token` (required) - CI project token
-- `project_url` (required) - CI project URL
+- `token` (**required**) - GitLab CI project specific token
+- `project_url` (**required**) - http://ci.gitlabhq.com/projects/3
+- `enable_ssl_verification` (optional) - Enable SSL verification
### Delete GitLab CI service
-Delete GitLab CI service settings for a project.
+Delete GitLab CI service for a project.
```
DELETE /projects/:id/services/gitlab-ci
```
+### Get GitLab CI service settings
+
+Get GitLab CI service settings for a project.
+
+```
+GET /projects/:id/services/gitlab-ci
+```
+
## HipChat
-### Edit HipChat service
+Private group chat and IM
+
+### Create/Edit HipChat service
-Set HipChat service for project.
+Set HipChat service for a project.
```
PUT /projects/:id/services/hipchat
```
+
Parameters:
-- `token` (required) - HipChat token
-- `room` (required) - HipChat room name
+- `token` (**required**) - Room token
+- `color` (optional)
+- `notify` (optional)
+- `room` (optional) - Room name or ID
+- `api_version` (optional) - Leave blank for default (v2)
+- `server` (optional) - Leave blank for default. https://hipchat.example.com
### Delete HipChat service
@@ -44,3 +436,261 @@ Delete HipChat service for a project.
```
DELETE /projects/:id/services/hipchat
```
+
+### Get HipChat service settings
+
+Get HipChat service settings for a project.
+
+```
+GET /projects/:id/services/hipchat
+```
+
+## Irker (IRC gateway)
+
+Send IRC messages, on update, to a list of recipients through an Irker gateway.
+
+### Create/Edit Irker (IRC gateway) service
+
+Set Irker (IRC gateway) service for a project.
+
+> NOTE: Irker does NOT have built-in authentication, which makes it vulnerable to spamming IRC channels if it is hosted outside of a firewall. Please make sure you run the daemon within a secured network to prevent abuse. For more details, read: http://www.catb.org/~esr/irker/security.html.
+
+```
+PUT /projects/:id/services/irker
+```
+
+Parameters:
+
+- `recipients` (**required**) - Recipients/channels separated by whitespaces
+- `default_irc_uri` (optional) - irc://irc.network.net:6697/
+- `server_port` (optional) - 6659
+- `server_host` (optional) - localhost
+- `colorize_messages` (optional)
+
+### Delete Irker (IRC gateway) service
+
+Delete Irker (IRC gateway) service for a project.
+
+```
+DELETE /projects/:id/services/irker
+```
+
+### Get Irker (IRC gateway) service settings
+
+Get Irker (IRC gateway) service settings for a project.
+
+```
+GET /projects/:id/services/irker
+```
+
+## JIRA
+
+Jira issue tracker
+
+### Create/Edit JIRA service
+
+Set JIRA service for a project.
+
+> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://doc.gitlab.com/ee/integration/jira.html)
+
+```
+PUT /projects/:id/services/jira
+```
+
+Parameters:
+
+- `new_issue_url` (**required**) - New Issue url
+- `project_url` (**required**) - Project url
+- `issues_url` (**required**) - Issue url
+- `description` (optional) - Jira issue tracker
+
+### Delete JIRA service
+
+Delete JIRA service for a project.
+
+```
+DELETE /projects/:id/services/jira
+```
+
+### Get JIRA service settings
+
+Get JIRA service settings for a project.
+
+```
+GET /projects/:id/services/jira
+```
+
+## PivotalTracker
+
+Project Management Software (Source Commits Endpoint)
+
+### Create/Edit PivotalTracker service
+
+Set PivotalTracker service for a project.
+
+```
+PUT /projects/:id/services/pivotaltracker
+```
+
+Parameters:
+
+- `token` (**required**)
+
+### Delete PivotalTracker service
+
+Delete PivotalTracker service for a project.
+
+```
+DELETE /projects/:id/services/pivotaltracker
+```
+
+### Get PivotalTracker service settings
+
+Get PivotalTracker service settings for a project.
+
+```
+GET /projects/:id/services/pivotaltracker
+```
+
+## Pushover
+
+Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop.
+
+### Create/Edit Pushover service
+
+Set Pushover service for a project.
+
+```
+PUT /projects/:id/services/pushover
+```
+
+Parameters:
+
+- `api_key` (**required**) - Your application key
+- `user_key` (**required**) - Your user key
+- `priority` (**required**)
+- `device` (optional) - Leave blank for all active devices
+- `sound` (optional)
+
+### Delete Pushover service
+
+Delete Pushover service for a project.
+
+```
+DELETE /projects/:id/services/pushover
+```
+
+### Get Pushover service settings
+
+Get Pushover service settings for a project.
+
+```
+GET /projects/:id/services/pushover
+```
+
+## Redmine
+
+Redmine issue tracker
+
+### Create/Edit Redmine service
+
+Set Redmine service for a project.
+
+```
+PUT /projects/:id/services/redmine
+```
+
+Parameters:
+
+- `new_issue_url` (**required**) - New Issue url
+- `project_url` (**required**) - Project url
+- `issues_url` (**required**) - Issue url
+- `description` (optional) - Redmine issue tracker
+
+### Delete Redmine service
+
+Delete Redmine service for a project.
+
+```
+DELETE /projects/:id/services/redmine
+```
+
+### Get Redmine service settings
+
+Get Redmine service settings for a project.
+
+```
+GET /projects/:id/services/redmine
+```
+
+## Slack
+
+A team communication tool for the 21st century
+
+### Create/Edit Slack service
+
+Set Slack service for a project.
+
+```
+PUT /projects/:id/services/slack
+```
+
+Parameters:
+
+- `webhook` (**required**) - https://hooks.slack.com/services/...
+- `username` (optional) - username
+- `channel` (optional) - #channel
+
+### Delete Slack service
+
+Delete Slack service for a project.
+
+```
+DELETE /projects/:id/services/slack
+```
+
+### Get Slack service settings
+
+Get Slack service settings for a project.
+
+```
+GET /projects/:id/services/slack
+```
+
+## JetBrains TeamCity CI
+
+A continuous integration and build server
+
+### Create/Edit JetBrains TeamCity CI service
+
+Set JetBrains TeamCity CI service for a project.
+
+> The build configuration in Teamcity must use the build format number %build.vcs.number% you will also want to configure monitoring of all branches so merge requests build, that setting is in the vsc root advanced settings.
+
+```
+PUT /projects/:id/services/teamcity
+```
+
+Parameters:
+
+- `teamcity_url` (**required**) - TeamCity root URL like https://teamcity.example.com
+- `build_type` (**required**) - Build configuration ID
+- `username` (**required**) - A user with permissions to trigger a manual build
+- `password` (**required**)
+
+### Delete JetBrains TeamCity CI service
+
+Delete JetBrains TeamCity CI service for a project.
+
+```
+DELETE /projects/:id/services/teamcity
+```
+
+### Get JetBrains TeamCity CI service settings
+
+Get JetBrains TeamCity CI service settings for a project.
+
+```
+GET /projects/:id/services/teamcity
+```
+
diff --git a/doc/ci/README.md b/doc/ci/README.md
new file mode 100644
index 00000000000..97325069ceb
--- /dev/null
+++ b/doc/ci/README.md
@@ -0,0 +1,23 @@
+## GitLab CI Documentation
+
+### User documentation
+
++ [Quick Start](quick_start/README.md)
++ [Configuring project (.gitlab-ci.yml)](yaml/README.md)
++ [Configuring runner](runners/README.md)
++ [Configuring deployment](deployment/README.md)
++ [Using Docker Images](docker/using_docker_images.md)
++ [Using Docker Build](docker/using_docker_build.md)
++ [Using Variables](variables/README.md)
+
+### Examples
+
++ [Test and deploy Ruby applications to Heroku](examples/test-and-deploy-ruby-application-to-heroku.md)
++ [Test and deploy Python applications to Heroku](examples/test-and-deploy-python-application-to-heroku.md)
++ [Test Clojure applications](examples/test-clojure-application.md)
++ Help your favorite programming language and GitLab by sending a merge request with a guide for that language.
+
+### Administrator documentation
+
++ [User permissions](permissions/README.md)
++ [API](api/README.md)
diff --git a/doc/ci/api/README.md b/doc/ci/api/README.md
new file mode 100644
index 00000000000..33c5b172e98
--- /dev/null
+++ b/doc/ci/api/README.md
@@ -0,0 +1,86 @@
+# GitLab CI API
+
+## Resources
+
+- [Projects](projects.md)
+- [Runners](runners.md)
+- [Commits](commits.md)
+- [Builds](builds.md)
+
+
+## Authentication
+
+GitLab CI API uses different types of authentication depends on what API you use.
+Each API document has section with information about authentication you need to use.
+
+GitLab CI API has 4 authentication methods:
+
+* GitLab user token & GitLab url
+* GitLab CI project token
+* GitLab CI runners registration token
+* GitLab CI runner token
+
+
+### Authentication #1: GitLab user token & GitLab url
+
+Authentication is done by
+sending the `private-token` of a valid user and the `url` of an
+authorized Gitlab instance via a query string along with the API
+request:
+
+ GET http://gitlab.example.com/ci/api/v1/projects?private_token=QVy1PB7sTxfy4pqfZM1U&url=http://demo.gitlab.com/
+
+If preferred, you may instead send the `private-token` as a header in
+your request:
+
+ curl --header "PRIVATE-TOKEN: QVy1PB7sTxfy4pqfZM1U" "http://gitlab.example.com/ci/api/v1/projects?url=http://demo.gitlab.com/"
+
+
+### Authentication #2: GitLab CI project token
+
+Each project in GitLab CI has it own token.
+It can be used to get project commits and builds information.
+You can use project token only for certain project.
+
+### Authentication #3: GitLab CI runners registration token
+
+This token is not persisted and is generated on each application start.
+It can be used only for registering new runners in system. You can find it on
+GitLab CI Runners web page https://gitlab-ci.example.com/admin/runners
+
+### Authentication #4: GitLab CI runner token
+
+Every GitLab CI runner has it own token that allow it to receive and update
+GitLab CI builds. This token exists of internal purposes and should be used only
+by runners
+
+## JSON
+
+All API requests are serialized using JSON. You don't need to specify
+`.json` at the end of API URL.
+
+## Status codes
+
+The API is designed to return different status codes according to context and action. In this way if a request results in an error the caller is able to get insight into what went wrong, e.g. status code `400 Bad Request` is returned if a required attribute is missing from the request. The following list gives an overview of how the API functions generally behave.
+
+API request types:
+
+- `GET` requests access one or more resources and return the result as JSON
+- `POST` requests return `201 Created` if the resource is successfully created and return the newly created resource as JSON
+- `GET`, `PUT` and `DELETE` return `200 OK` if the resource is accessed, modified or deleted successfully, the (modified) result is returned as JSON
+- `DELETE` requests are designed to be idempotent, meaning a request a resource still returns `200 OK` even it was deleted before or is not available. The reasoning behind it is the user is not really interested if the resource existed before or not.
+
+The following list shows the possible return codes for API requests.
+
+Return values:
+
+- `200 OK` - The `GET`, `PUT` or `DELETE` request was successful, the resource(s) itself is returned as JSON
+- `201 Created` - The `POST` request was successful and the resource is returned as JSON
+- `400 Bad Request` - A required attribute of the API request is missing, e.g. the title of an issue is not given
+- `401 Unauthorized` - The user is not authenticated, a valid user token is necessary, see above
+- `403 Forbidden` - The request is not allowed, e.g. the user is not allowed to delete a project
+- `404 Not Found` - A resource could not be accessed, e.g. an ID for a resource could not be found
+- `405 Method Not Allowed` - The request is not supported
+- `409 Conflict` - A conflicting resource already exists, e.g. creating a project with a name that already exists
+- `422 Unprocessable` - The entity could not be processed
+- `500 Server Error` - While handling the request something went wrong on the server side
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
new file mode 100644
index 00000000000..3b5008ccdb4
--- /dev/null
+++ b/doc/ci/api/builds.md
@@ -0,0 +1,41 @@
+# Builds API
+
+This API used by runners to receive and update builds.
+
+__Authentication is done by runner token__
+
+## Builds
+
+### Runs oldest pending build by runner
+
+ POST /ci/builds/register
+
+Parameters:
+
+ * `token` (required) - The unique token of runner
+
+Returns:
+
+```json
+{
+ "id" : 79,
+ "commands" : "",
+ "path" : "",
+ "ref" : "",
+ "sha" : "",
+ "project_id" : 6,
+ "repo_url" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "before_sha" : ""
+}
+```
+
+
+### Update details of an existing build
+
+ PUT /ci/builds/:id
+
+Parameters:
+
+ * `id` (required) - The ID of a project
+ * `state` (optional) - The state of a build
+ * `trace` (optional) - The trace of a build
diff --git a/doc/ci/api/commits.md b/doc/ci/api/commits.md
new file mode 100644
index 00000000000..4df7afc6c52
--- /dev/null
+++ b/doc/ci/api/commits.md
@@ -0,0 +1,101 @@
+# Commits API
+
+__Authentication is done by GitLab CI project token__
+
+## Commits
+
+### Retrieve all commits per project
+
+Get list of commits per project
+
+ GET /ci/commits
+
+Parameters:
+
+ * `project_id` (required) - The ID of a project
+ * `project_token` (requires) - Project token
+ * `page` (optional)
+ * `per_page` (optional) - items per request (default is 20)
+
+Returns:
+
+```json
+[{
+ "id": 3,
+ "ref": "master",
+ "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
+ "project_id": 2,
+ "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
+ "created_at": "2014-11-05T09:46:35.247Z",
+ "status": "success",
+ "finished_at": "2014-11-05T09:46:44.254Z",
+ "duration": 5.062692165374756,
+ "git_commit_message": "wow\n",
+ "git_author_name": "Administrator",
+ "git_author_email": "admin@example.com",
+ "builds": [{
+ "id": 7,
+ "project_id": 2,
+ "ref": "master",
+ "status": "success",
+ "finished_at": "2014-11-05T09:46:44.254Z",
+ "created_at": "2014-11-05T09:46:35.259Z",
+ "updated_at": "2014-11-05T09:46:44.255Z",
+ "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
+ "started_at": "2014-11-05T09:46:39.192Z",
+ "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
+ "runner_id": 1,
+ "coverage": null,
+ "commit_id": 3
+ }]
+}]
+```
+
+### Create commit
+
+Inform GitLab CI about new commit you want it to build.
+
+__If commit already exists in GitLab CI it will not be created__
+
+
+ POST /ci/commits
+
+Parameters:
+
+ * `project_id` (required) - The ID of a project
+ * `project_token` (requires) - Project token
+ * `data` (required) - Push data. For example see comment in `lib/api/commits.rb`
+
+Returns:
+
+```json
+{
+ "id": 3,
+ "ref": "master",
+ "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
+ "project_id": 2,
+ "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
+ "created_at": "2014-11-05T09:46:35.247Z",
+ "status": "success",
+ "finished_at": "2014-11-05T09:46:44.254Z",
+ "duration": 5.062692165374756,
+ "git_commit_message": "wow\n",
+ "git_author_name": "Administrator",
+ "git_author_email": "admin@example.com",
+ "builds": [{
+ "id": 7,
+ "project_id": 2,
+ "ref": "master",
+ "status": "success",
+ "finished_at": "2014-11-05T09:46:44.254Z",
+ "created_at": "2014-11-05T09:46:35.259Z",
+ "updated_at": "2014-11-05T09:46:44.255Z",
+ "sha": "65617dfc36761baa1f46a7006f2a88916f7f56cf",
+ "started_at": "2014-11-05T09:46:39.192Z",
+ "before_sha": "96906f2bceb04c7323f8514aa5ad8cb1313e2898",
+ "runner_id": 1,
+ "coverage": null,
+ "commit_id": 3
+ }]
+}
+```
diff --git a/doc/ci/api/projects.md b/doc/ci/api/projects.md
new file mode 100644
index 00000000000..5585191e826
--- /dev/null
+++ b/doc/ci/api/projects.md
@@ -0,0 +1,149 @@
+# Projects API
+
+This API is intended to aid in the setup and configuration of
+projects on Gitlab CI.
+
+__Authentication is done by GitLab user token & GitLab url__
+
+## Projects
+
+### List Authorized Projects
+
+Lists all projects that the authenticated user has access to.
+
+```
+GET /ci/projects
+```
+
+Returns:
+
+```json
+ [
+ {
+ "id" : 271,
+ "name" : "gitlabhq",
+ "timeout" : 1800,
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "path" : "gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 3
+ },
+ {
+ "id" : 272,
+ "name" : "gitlab-ci",
+ "timeout" : 1800,
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "path" : "gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 4
+ }
+]
+```
+
+### List Owned Projects
+
+Lists all projects that the authenticated user owns.
+
+```
+GET /ci/projects/owned
+```
+
+Returns:
+
+```json
+[
+ {
+ "id" : 272,
+ "name" : "gitlab-ci",
+ "timeout" : 1800,
+ "token" : "iPWx6WM4lhHNedGfBpPJNP",
+ "default_ref" : "master",
+ "gitlab_url" : "http://demo.gitlabhq.com/gitlab/gitlab-shell",
+ "path" : "gitlab/gitlab-shell",
+ "always_build" : false,
+ "polling_interval" : null,
+ "public" : false,
+ "ssh_url_to_repo" : "git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "gitlab_id" : 4
+ }
+]
+```
+
+### Single Project
+
+Returns information about a single project for which the user is
+authorized.
+
+ GET /ci/projects/:id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+
+### Create Project
+
+Creates a Gitlab CI project using Gitlab project details.
+
+ POST /ci/projects
+
+Parameters:
+
+ * `name` (required) - The name of the project
+ * `gitlab_id` (required) - The ID of the project on the Gitlab instance
+ * `default_ref` (optional) - The branch to run on (default to `master`)
+
+### Update Project
+
+Updates a Gitlab CI project using Gitlab project details that the
+authenticated user has access to.
+
+ PUT /ci/projects/:id
+
+Parameters:
+
+ * `name` - The name of the project
+ * `default_ref` - The branch to run on (default to `master`)
+
+### Remove Project
+
+Removes a Gitlab CI project that the authenticated user has access to.
+
+ DELETE /ci/projects/:id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+
+### Link Project to Runner
+
+Links a runner to a project so that it can make builds (only via
+authorized user).
+
+ POST /ci/projects/:id/runners/:runner_id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+ * `runner_id` (required) - The ID of the Gitlab CI runner
+
+### Remove Project from Runner
+
+Removes a runner from a project so that it can not make builds (only
+via authorized user).
+
+ DELETE /ci/projects/:id/runners/:runner_id
+
+Parameters:
+
+ * `id` (required) - The ID of the Gitlab CI project
+ * `runner_id` (required) - The ID of the Gitlab CI runner \ No newline at end of file
diff --git a/doc/ci/api/runners.md b/doc/ci/api/runners.md
new file mode 100644
index 00000000000..e9f88ee066e
--- /dev/null
+++ b/doc/ci/api/runners.md
@@ -0,0 +1,77 @@
+# Runners API
+
+## Runners
+
+### Retrieve all runners
+
+__Authentication is done by GitLab user token & GitLab url__
+
+Used to get information about all runners registered on the Gitlab CI
+instance.
+
+ GET /ci/runners
+
+Returns:
+
+```json
+[
+ {
+ "id" : 85,
+ "token" : "12b68e90394084703135"
+ },
+ {
+ "id" : 86,
+ "token" : "76bf894e969364709864"
+ },
+]
+```
+
+### Register a new runner
+
+
+__Authentication is done with a Shared runner registration token or a project Specific runner registration token__
+
+Used to make Gitlab CI aware of available runners.
+
+ POST /ci/runners/register
+
+Parameters:
+
+ * `token` (required) - The registration token. It is 2 types of token you can pass here.
+
+1. Shared runner registration token
+2. Project specific registration token
+
+Returns:
+
+```json
+{
+ "id" : 85,
+ "token" : "12b68e90394084703135"
+}
+```
+
+### Delete a runner
+
+
+__Authentication is done by runner token__
+
+Used to removing runners.
+
+ DELETE /ci/runners/delete
+
+Parameters:
+
+ * `token` (required) - The runner token.
+
+Returns:
+
+```json
+{
+ "id" : 1,
+ "token" : "d14963981a428f70121777e50643d1",
+ "created_at" : "2015-02-26T11:39:39.232Z",
+ "updated_at" : "2015-02-26T11:39:39.232Z",
+ "description" : "awesome runner"
+}
+``` \ No newline at end of file
diff --git a/doc/ci/deployment/README.md b/doc/ci/deployment/README.md
new file mode 100644
index 00000000000..ffd841ca9e7
--- /dev/null
+++ b/doc/ci/deployment/README.md
@@ -0,0 +1,98 @@
+## Using Dpl as deployment tool
+Dpl (dee-pee-ell) is a deploy tool made for continuous deployment that's developed and used by Travis CI, but can also be used with GitLab CI.
+
+**We recommend to use Dpl, if you're deploying to any of these of these services: https://github.com/travis-ci/dpl#supported-providers**.
+
+### Requirements
+To use Dpl you need at least Ruby 1.8.7 with ability to install gems.
+
+### Basic usage
+The Dpl can be installed on any machine with:
+```
+gem install dpl
+```
+
+This allows you to test all commands from your shell, rather than having to test it on a CI server.
+
+If you don't have Ruby installed you can do it on Debian-compatible Linux with:
+```
+apt-get update
+apt-get install ruby-dev
+```
+
+The Dpl provides support for vast number of services, including: Heroku, Cloud Foundry, AWS/S3, and more.
+To use it simply define provider and any additional parameters required by the provider.
+
+For example if you want to use it to deploy your application to heroku, you need to specify `heroku` as provider, specify `api-key` and `app`.
+There's more and all possible parameters can be found here: https://github.com/travis-ci/dpl#heroku
+
+```
+staging:
+ type: deploy
+ - gem install dpl
+ - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
+```
+
+In the above example we use Dpl to deploy `my-app-staging` to Heroku server with api-key stored in `HEROKU_STAGING_API_KEY` secure variable.
+
+To use different provider take a look at long list of [Supported Providers](https://github.com/travis-ci/dpl#supported-providers).
+
+### Using Dpl with Docker
+When you use GitLab Runner you most likely configured it to use your server's shell commands.
+This means that all commands are run in context of local user (ie. gitlab_runner or gitlab_ci_multi_runner).
+It also means that most probably in your Docker container you don't have the Ruby runtime installed.
+You will have to install it:
+```
+staging:
+ type: deploy
+ - apt-get update -yq
+ - apt-get install -y ruby-dev
+ - gem install dpl
+ - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
+ only:
+ - master
+```
+
+The first line `apt-get update -yq` updates the list of available packages, where second `apt-get install -y ruby-dev` install `Ruby` runtime on system.
+The above example is valid for all Debian-compatible systems.
+
+### Usage in staging and production
+It's pretty common in developer workflow to have staging (development) and production environment.
+If we consider above example: we would like to deploy `master` branch to `staging` and `all tags` to `production` environment.
+The final `.gitlab-ci.yml` for that setup would look like this:
+
+```
+staging:
+ type: deploy
+ - gem install dpl
+ - dpl --provider=heroku --app=my-app-staging --api-key=$HEROKU_STAGING_API_KEY
+ only:
+ - master
+
+production:
+ type: deploy
+ - gem install dpl
+ - dpl --provider=heroku --app=my-app-production --api-key=$HEROKU_PRODUCTION_API_KEY
+ only:
+ - tags
+```
+
+We created two deploy jobs that are executed on different events:
+1. `staging` is executed for all commits that were pushed to `master` branch,
+2. `production` is executed for all pushed tags.
+
+We also use two secure 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.
+
+### Storing API keys
+In GitLab CI 7.12 a new feature was introduced: Secure Variables.
+Secure Variables can added by going to `Project > 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..5af27470d2f
--- /dev/null
+++ b/doc/ci/docker/using_docker_build.md
@@ -0,0 +1,111 @@
+# Using Docker Build
+
+GitLab CI 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://gitlab.com/ci \
+ --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://gitlab.com/ci \
+ --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, check out [Runtime privilege](https://docs.docker.com/reference/run/#runtime-privilege-linux-capabilities-and-lxc-configuration). \ No newline at end of file
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
new file mode 100644
index 00000000000..191e3a8144d
--- /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://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 <<EOF > 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..036b03dd6b9
--- /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 `gitlab.com/ci`, but you can register your own:
+```
+gitlab-ci-multi-runner register \
+ --non-interactive \
+ --url "https://gitlab.com/ci/" \
+ --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..d2a872f1934
--- /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 `gitlab.com/ci`, but you can register your own:
+```
+gitlab-ci-multi-runner register \
+ --non-interactive \
+ --url "https://gitlab.com/ci/" \
+ --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..eaee94a10f1
--- /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 `gitlab.com` for testing your application with such configuration.
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..a87a1f806fc
--- /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 or use [gitlab.com/ci](http://gitlab.com/ci/).
+
+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://gitlab.com/ci/
+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 [gitlab.com/ci](http://gitlab.com/ci/) 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 00000000000..333259e6acd
--- /dev/null
+++ b/doc/ci/quick_start/build_status.png
Binary files differ
diff --git a/doc/ci/quick_start/commit_status.png b/doc/ci/quick_start/commit_status.png
new file mode 100644
index 00000000000..725b79e6f91
--- /dev/null
+++ b/doc/ci/quick_start/commit_status.png
Binary files differ
diff --git a/doc/ci/quick_start/new_commit.png b/doc/ci/quick_start/new_commit.png
new file mode 100644
index 00000000000..3839e893c17
--- /dev/null
+++ b/doc/ci/quick_start/new_commit.png
Binary files differ
diff --git a/doc/ci/quick_start/projects.png b/doc/ci/quick_start/projects.png
new file mode 100644
index 00000000000..0b3430a69db
--- /dev/null
+++ b/doc/ci/quick_start/projects.png
Binary files differ
diff --git a/doc/ci/quick_start/runners.png b/doc/ci/quick_start/runners.png
new file mode 100644
index 00000000000..25b4046bc00
--- /dev/null
+++ b/doc/ci/quick_start/runners.png
Binary files differ
diff --git a/doc/ci/quick_start/runners_activated.png b/doc/ci/quick_start/runners_activated.png
new file mode 100644
index 00000000000..c934bd12f41
--- /dev/null
+++ b/doc/ci/quick_start/runners_activated.png
Binary files differ
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 00000000000..f51ea694e78
--- /dev/null
+++ b/doc/ci/runners/project_specific.png
Binary files differ
diff --git a/doc/ci/runners/shared_runner.png b/doc/ci/runners/shared_runner.png
new file mode 100644
index 00000000000..9755144eb08
--- /dev/null
+++ b/doc/ci/runners/shared_runner.png
Binary files differ
diff --git a/doc/ci/runners/shared_to_specific_admin.png b/doc/ci/runners/shared_to_specific_admin.png
new file mode 100644
index 00000000000..44a4bef22f7
--- /dev/null
+++ b/doc/ci/runners/shared_to_specific_admin.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
new file mode 100644
index 00000000000..04c6bf1e3a3
--- /dev/null
+++ b/doc/ci/variables/README.md
@@ -0,0 +1,95 @@
+## Variables
+When receiving a build from GitLab CI, the runner prepares the build environment.
+It starts by setting a list of **predefined variables** (Environment Variables) and a list of **user-defined variables**
+
+The variables can be overwritten. They take precedence over each other in this order:
+1. Secure variables
+1. YAML-defined variables
+1. Predefined variables
+
+For example, if you define:
+1. API_TOKEN=SECURE as Secure Variable
+1. API_TOKEN=YAML as YAML-defined variable
+
+The API_TOKEN will take the Secure Variable value: `SECURE`.
+
+### Predefined variables (Environment Variables)
+
+| Variable | Description |
+|-------------------------|-------------|
+| **CI** | Mark that build is executed in CI environment |
+| **GITLAB_CI** | Mark that build is executed in GitLab CI environment |
+| **CI_SERVER** | Mark that build is executed in CI environment |
+| **CI_SERVER_NAME** | CI server that is used to coordinate builds |
+| **CI_SERVER_VERSION** | Not yet defined |
+| **CI_SERVER_REVISION** | Not yet defined |
+| **CI_BUILD_REF** | The commit revision for which project is built |
+| **CI_BUILD_BEFORE_SHA** | The first commit that were included in push request |
+| **CI_BUILD_REF_NAME** | The branch or tag name for which project is built |
+| **CI_BUILD_ID** | The unique id of the current build that GitLab CI uses internally |
+| **CI_BUILD_REPO** | The URL to clone the Git repository |
+| **CI_PROJECT_ID** | The unique id of the current project that GitLab CI uses internally |
+| **CI_PROJECT_DIR** | The full path where the repository is cloned and where the build is ran |
+
+Example values:
+
+```bash
+export CI_BUILD_BEFORE_SHA="9df57456fa9de2a6d335ca5edf9750ed812b9df0"
+export CI_BUILD_ID="50"
+export CI_BUILD_REF="1ecfd275763eff1d6b4844ea3168962458c9f27a"
+export CI_BUILD_REF_NAME="master"
+export CI_BUILD_REPO="https://gitlab.com/gitlab-org/gitlab-ce.git"
+export CI_PROJECT_DIR="/builds/gitlab-org/gitlab-ce"
+export CI_PROJECT_ID="34"
+export CI_SERVER="yes"
+export CI_SERVER_NAME="GitLab CI"
+export CI_SERVER_REVISION=""
+export CI_SERVER_VERSION=""
+```
+
+### YAML-defined variables
+**This feature requires GitLab Runner 0.5.0 or higher**
+
+GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment.
+The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL.
+
+```yaml
+variables:
+ DATABASE_URL: "postgres://postgres@postgres/my_database"
+```
+
+These variables can be later used in all executed commands and scripts.
+
+The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them.
+
+More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md).
+
+### User-defined variables (Secure Variables)
+**This feature requires GitLab Runner 0.4.0 or higher**
+
+GitLab CI allows you to define per-project **Secure Variables** that are set in build environment.
+The secure variables are stored out of the repository (the `.gitlab-ci.yml`).
+These variables are securely stored in GitLab CI database and are hidden in the build log.
+It's desired method to use them for storing passwords, secret keys or whatever you want.
+
+Secure Variables can added by going to `Project > Variables > Add Variable`.
+
+They will be available for all subsequent builds.
+
+### Use variables
+The variables are set as environment variables in build environment and are accessible with normal methods that are used to access such variables.
+In most cases the **bash** is used to execute build script.
+To access variables (predefined and user-defined) in bash environment, prefix the variable name with `$`:
+```
+job_name:
+ script:
+ - echo $CI_BUILD_ID
+```
+
+You can also list all environment variables with `export` command,
+but be aware that this will also expose value of all **Secure Variables** in build log:
+```
+job_name:
+ script:
+ - export
+```
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
new file mode 100644
index 00000000000..4caeccacb7f
--- /dev/null
+++ b/doc/ci/yaml/README.md
@@ -0,0 +1,204 @@
+# Configuration of your builds with .gitlab-ci.yml
+From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML) file (**.gitlab-ci.yml**) for the project configuration.
+It is placed in the root of your repository and contains definitions of how your project should be built.
+
+The YAML file defines a set of jobs with constraints stating when they should be run.
+The jobs are defined as top-level elements with a name and always have to contain the `script` clause:
+
+```yaml
+job1:
+ script: "execute-script-for-job1"
+
+job2:
+ script: "execute-script-for-job2"
+```
+
+The above example is the simplest possible CI configuration with two separate jobs,
+where each of the jobs executes a different command.
+Of course a command can execute code directly (`./configure;make;make install`) or run a script (`test.sh`) in the repository.
+
+Jobs are used to create builds, which are then picked up by [runners](../runners/README.md) and executed within the environment of the runner.
+What is important, is that each job is run independently from each other.
+
+## .gitlab-ci.yml
+The YAML syntax allows for using more complex job specifications than in the above example:
+
+```yaml
+image: ruby:2.1
+services:
+ - postgres
+
+before_script:
+ - bundle_install
+
+stages:
+ - build
+ - test
+ - deploy
+
+job1:
+ stage: build
+ script:
+ - execute-script-for-job1
+ only:
+ - master
+ tags:
+ - docker
+```
+
+There are a few `keywords` that can't be used as job names:
+
+| keyword | required | description |
+|---------------|----------|-------------|
+| image | optional | Use docker image, covered in [Use Docker](../docker/README.md) |
+| services | optional | Use docker services, covered in [Use Docker](../docker/README.md) |
+| stages | optional | Define build stages |
+| types | optional | Alias for `stages` |
+| before_script | optional | Define commands prepended for each job's script |
+| variables | optional | Define build variables |
+
+### image and services
+This allows to specify a custom Docker image and a list of services that can be used for time of the build.
+The configuration of this feature is covered in separate document: [Use Docker](../docker/README.md).
+
+### before_script
+`before_script` is used to define the command that should be run before all builds, including deploy builds. This can be an array or a multiline string.
+
+### stages
+`stages` is used to define build stages that can be used by jobs.
+The specification of `stages` allows for having flexible multi stage pipelines.
+
+The ordering of elements in `stages` defines the ordering of builds' execution:
+
+1. Builds of the same stage are run in parallel.
+1. Builds of next stage are run after success.
+
+Let's consider the following example, which defines 3 stages:
+```
+stages:
+ - build
+ - test
+ - deploy
+```
+
+1. First all jobs of `build` are executed in parallel.
+1. If all jobs of `build` succeeds, the `test` jobs are executed in parallel.
+1. If all jobs of `test` succeeds, the `deploy` jobs are executed in parallel.
+1. If all jobs of `deploy` succeeds, the commit is marked as `success`.
+1. If any of the previous jobs fails, the commit is marked as `failed` and no jobs of further stage are executed.
+
+There are also two edge cases worth mentioning:
+
+1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`, `test` and `deploy` are allowed to be used as job's stage by default.
+2. If a job doesn't specify `stage`, the job is assigned the `test` stage.
+
+### types
+Alias for [stages](#stages).
+
+### variables
+**This feature requires `gitlab-runner` with version equal or greater than 0.5.0.**
+
+GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build environment.
+The variables are stored in repository and are meant to store non-sensitive project configuration, ie. RAILS_ENV or DATABASE_URL.
+
+```yaml
+variables:
+ DATABASE_URL: "postgres://postgres@postgres/my_database"
+```
+
+These variables can be later used in all executed commands and scripts.
+
+The YAML-defined variables are also set to all created service containers, thus allowing to fine tune them.
+
+## Jobs
+`.gitlab-ci.yml` allows you to specify an unlimited number of jobs.
+Each job has to have a unique `job_name`, which is not one of the keywords mentioned above.
+A job is defined by a list of parameters that define the build behaviour.
+
+```yaml
+job_name:
+ script:
+ - rake spec
+ - coverage
+ stage: test
+ only:
+ - master
+ except:
+ - develop
+ tags:
+ - ruby
+ - postgres
+ allow_failure: true
+```
+
+| keyword | required | description |
+|---------------|----------|-------------|
+| script | required | Defines a shell script which is executed by runner |
+| stage | optional (default: test) | Defines a build stage |
+| type | optional | Alias for `stage` |
+| only | optional | Defines a list of git refs for which build is created |
+| except | optional | Defines a list of git refs for which build is not created |
+| tags | optional | Defines a list of tags which are used to select runner |
+| allow_failure | optional | Allow build to fail. Failed build doesn't contribute to commit status |
+
+### script
+`script` is a shell script which is executed by runner. The shell script is prepended with `before_script`.
+
+```yaml
+job:
+ script: "bundle exec rspec"
+```
+
+This parameter can also contain several commands using an array:
+```yaml
+job:
+ script:
+ - uname -a
+ - bundle exec rspec
+```
+
+### stage
+`stage` allows to group build into different stages. Builds of the same `stage` are executed in `parallel`.
+For more info about the use of `stage` please check the [stages](#stages).
+
+### only and except
+This are two parameters that allow for setting a refs policy to limit when jobs are built:
+1. `only` defines the names of branches and tags for which job will be built.
+2. `except` defines the names of branches and tags for which the job wil **not** be built.
+
+There are a few rules that apply to usage of refs policy:
+
+1. `only` and `except` are exclusive. If both `only` and `except` are defined in job specification only `only` is taken into account.
+1. `only` and `except` allow for using the regexp expressions.
+1. `only` and `except` allow for using special keywords: `branches` and `tags`.
+These names can be used for example to exclude all tags and all branches.
+
+```yaml
+job:
+ only:
+ - /^issue-.*$/ # use regexp
+ except:
+ - branches # use special keyword
+```
+
+### tags
+`tags` is used to select specific runners from the list of all runners that are allowed to run this project.
+
+During registration of a runner, you can specify the runner's tags, ie.: `ruby`, `postgres`, `development`.
+`tags` allow you to run builds with runners that have the specified tags assigned:
+
+```
+job:
+ tags:
+ - ruby
+ - postgres
+```
+
+The above specification will make sure that `job` is built by a runner that have `ruby` AND `postgres` tags defined.
+
+## Validate the .gitlab-ci.yml
+Each instance of GitLab CI has an embedded debug tool called Lint.
+You can find the link to the Lint in the project's settings page or use short url `/lint`.
+
+## Skipping builds
+There is one more way to skip all builds, if your commit message contains tag [ci skip]. In this case, commit will be created but builds will be skipped \ No newline at end of file
diff --git a/doc/development/README.md b/doc/development/README.md
index 6bc8e1888db..d5bf166ad32 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -8,3 +8,4 @@
- [UI guide](ui_guide.md) for building GitLab with existing css styles and elements
- [Migration Style Guide](migration_style_guide.md) for creating safe migrations
- [How to dump production data to staging](dump_db.md)
+- [Benchmarking](benchmarking.md)
diff --git a/doc/development/benchmarking.md b/doc/development/benchmarking.md
new file mode 100644
index 00000000000..88e18ee95f9
--- /dev/null
+++ b/doc/development/benchmarking.md
@@ -0,0 +1,69 @@
+# Benchmarking
+
+GitLab CE comes with a set of benchmarks that are executed for every build. This
+makes it easier to measure performance of certain components over time.
+
+Benchmarks are written as RSpec tests using a few extra helpers. To write a
+benchmark, first tag the top-level `describe`:
+
+```ruby
+describe MaruTheCat, benchmark: true do
+
+end
+```
+
+This ensures the benchmark is executed separately from other test collections.
+It also exposes the various RSpec matchers used for writing benchmarks to the
+test group.
+
+Next, lets write the actual benchmark:
+
+```ruby
+describe MaruTheCat, benchmark: true do
+ let(:maru) { MaruTheChat.new }
+
+ describe '#jump_in_box' do
+ benchmark_subject { maru.jump_in_box }
+
+ it { is_expected.to iterate_per_second(9000) }
+ end
+end
+```
+
+Here `benchmark_subject` is a small wrapper around RSpec's `subject` method that
+makes it easier to specify the subject of a benchmark. Using RSpec's regular
+`subject` would require us to write the following instead:
+
+```ruby
+subject { -> { maru.jump_in_box } }
+```
+
+The `iterate_per_second` matcher defines the amount of times per second a
+subject should be executed. The higher the amount of iterations the better.
+
+By default the allowed standard deviation is a maximum of 30%. This can be
+adjusted by chaining the `with_maximum_stddev` on the `iterate_per_second`
+matcher:
+
+```ruby
+it { is_expected.to iterate_per_second(9000).with_maximum_stddev(50) }
+```
+
+This can be useful if the code in question depends on external resources of
+which the performance can vary a lot (e.g. physical HDDs, network calls, etc).
+However, in most cases 30% should be enough so only change this when really
+needed.
+
+## Benchmarks Location
+
+Benchmarks should be stored in `spec/benchmarks` and should follow the regular
+Rails specs structure. That is, model benchmarks go in `spec/benchmark/models`,
+benchmarks for code in the `lib` directory go in `spec/benchmarks/lib`, etc.
+
+## Underlying Technology
+
+The benchmark setup uses [benchmark-ips][benchmark-ips] which takes care of the
+heavy lifting such as warming up code, calculating iterations, standard
+deviation, etc.
+
+[benchmark-ips]: https://github.com/evanphx/benchmark-ips
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index 53f8095cb13..a4a980cf0e0 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -27,3 +27,9 @@ You can find results under the `doc/code` directory.
```
bundle exec rake gitlab:generate_docs
```
+
+## Generate API documentation for project services (e.g. Slack)
+
+```
+bundle exec rake services:doc
+```
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index b904c70e980..493e1d1b09c 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -23,3 +23,5 @@ Step-by-step guides on the basics of working with Git and GitLab.
* [Add an image](add-image.md)
* [Create a Merge Request](add-merge-request.md)
+
+* [Create an Issue](create-issue.md)
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
new file mode 100644
index 00000000000..87f078def04
--- /dev/null
+++ b/doc/gitlab-basics/create-issue.md
@@ -0,0 +1,27 @@
+# How to create an Issue in GitLab
+
+The Issue Tracker is a good place to add things that need to be improved or solved in a project.
+
+To create an Issue, sign in to GitLab.
+
+Go to the project where you'd like to create the Issue:
+
+![Select a project](basicsimages/select_project.png)
+
+Click on "Issues" on the left side of your screen:
+
+![Issues](basicsimages/issues.png)
+
+Click on the "+ new issue" button on the right side of your screen:
+
+![New issue](basicsimages/new_issue.png)
+
+Add a title and a description to your issue:
+
+![Issue title and description](basicsimages/issue_title.png)
+
+You may assign the Issue to a user, add a milestone and add labels (they are all optional). Then click on "submit new issue":
+
+![Submit new issue](basicsimages/submit_new_issue.png)
+
+Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](http://doc.gitlab.com/ce/customization/issue_closing.html).
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index c8a73feb028..f31c353f2cf 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -10,11 +10,7 @@ After you confirm, go to GitLab and sign in to your account.
## Add your SSH Key
-At the top right corner, click on "profile settings":
-
-![profile settings](basicsimages/profile_settings.png)
-
-On the left side menu click on "SSH Keys":
+On the left side menu, click on "profile settings" and then click on "SSH Keys":
![SSH Keys](basicsimages/shh_keys.png)
diff --git a/doc/hooks/custom_hooks.md b/doc/hooks/custom_hooks.md
index f7d4f3de68b..548c484bc08 100644
--- a/doc/hooks/custom_hooks.md
+++ b/doc/hooks/custom_hooks.md
@@ -2,7 +2,7 @@
**Note: Custom git hooks must be configured on the filesystem of the GitLab
server. Only GitLab server administrators will be able to complete these tasks.
-Please explore webhooks as an option if you do not have filesystem access.**
+Please explore webhooks as an option if you do not have filesystem access. For a user configurable Git Hooks interface, please see [GitLab Enterprise Edition Git Hooks](http://doc.gitlab.com/ee/git_hooks/git_hooks.html).**
Git natively supports hooks that are executed on different actions.
Examples of server-side git hooks include pre-receive, post-receive, and update.
diff --git a/doc/incoming_email/README.md b/doc/incoming_email/README.md
new file mode 100644
index 00000000000..316746ab54d
--- /dev/null
+++ b/doc/incoming_email/README.md
@@ -0,0 +1,211 @@
+# Reply by email
+
+GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails.
+
+**Warning**: Do not enable Reply by email if you have **multiple GitLab application servers**.
+Due to an issue with the way incoming emails are read from the mail server, every incoming reply-by-email email will result in as many comments being created as you have application servers.
+[A fix is being worked on.](https://github.com/tpitale/mail_room/issues/46)
+
+## Get a mailbox
+
+Reply by email requires an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix mail server which you can run on-premises.
+
+If you want to use Gmail with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
+
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow [these instructions](./postfix.md).
+
+## Set it up
+
+In this example, we'll use the Gmail address `gitlab-incoming@gmail.com`.
+
+### Omnibus package installations
+
+1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the feature, enter the email address including a placeholder for the `key` that references the item being replied to and fill in the details for your specific IMAP server and email account:
+
+ ```ruby
+ gitlab_rails['incoming_email_enabled'] = true
+ gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
+ gitlab_rails['incoming_email_host'] = "imap.gmail.com" # IMAP server host
+ gitlab_rails['incoming_email_port'] = 993 # IMAP server port
+ gitlab_rails['incoming_email_ssl'] = true # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com" # Email account username. Usually the full email address.
+ gitlab_rails['incoming_email_password'] = "password" # Email account password
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox" # The name of the mailbox where incoming mail will end up. Usually "inbox".
+ ```
+
+ As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`.
+
+1. Reconfigure GitLab for the changes to take effect:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ sudo gitlab-rake gitlab:incoming_email:check
+ ```
+
+1. Reply by email should now be working.
+
+### Installations from source
+
+1. Go to the GitLab installation directory:
+
+ ```sh
+ cd /home/git/gitlab
+ ```
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to:
+
+ ```sh
+ sudo editor config/gitlab.yml
+ ```
+
+ ```yaml
+ incoming_email:
+ enabled: true
+ address: "gitlab-incoming+%{key}@gmail.com"
+ ```
+
+ As mentioned, the part after `+` in the address is ignored, and any email sent here will end up in the mailbox for `gitlab-incoming@gmail.com`.
+
+2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`:
+
+ ```sh
+ sudo cp config/mail_room.yml.example config/mail_room.yml
+ ```
+
+3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account:
+
+ ```sh
+ sudo editor config/mail_room.yml
+ ```
+
+ ```yaml
+ :mailboxes:
+ -
+ # IMAP server host
+ :host: "imap.gmail.com"
+ # IMAP server port
+ :port: 993
+ # Whether the IMAP server uses SSL
+ :ssl: true
+ # Whether the IMAP server uses StartTLS
+ :start_tls: false
+ # Email account username. Usually the full email address.
+ :email: "gitlab-incoming@gmail.com"
+ # Email account password
+ :password: "[REDACTED]"
+ # The name of the mailbox where incoming mail will end up. Usually "inbox".
+ :name: "inbox"
+ # Always "sidekiq".
+ :delivery_method: sidekiq
+ # Always true.
+ :delete_after_delivery: true
+ :delivery_options:
+ # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml.
+ :redis_url: redis://localhost:6379
+ # Always "resque:gitlab".
+ :namespace: resque:gitlab
+ # Always "incoming_email".
+ :queue: incoming_email
+ # Always "EmailReceiverWorker"
+ :worker: EmailReceiverWorker
+ ```
+
+5. Edit the init script configuration at `/etc/default/gitlab` to enable `mail_room`:
+
+ ```sh
+ sudo mkdir -p /etc/default
+ echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
+ ```
+
+6. Restart GitLab:
+
+ ```sh
+ sudo service gitlab restart
+ ```
+
+7. Verify that everything is configured correctly:
+
+ ```sh
+ sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
+ ```
+
+8. Reply by email should now be working.
+
+### Development
+
+1. Go to the GitLab installation directory.
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `key` that references the item being replied to:
+
+ ```yaml
+ incoming_email:
+ enabled: true
+ address: "gitlab-incoming+%{key}@gmail.com"
+ ```
+
+ As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
+
+2. Copy `config/mail_room.yml.example` to `config/mail_room.yml`:
+
+ ```sh
+ sudo cp config/mail_room.yml.example config/mail_room.yml
+ ```
+
+3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account:
+
+ ```yaml
+ :mailboxes:
+ -
+ # IMAP server host
+ :host: "imap.gmail.com"
+ # IMAP server port
+ :port: 993
+ # Whether the IMAP server uses SSL
+ :ssl: true
+ # Whether the IMAP server uses StartTLS
+ :start_tls: false
+ # Email account username. Usually the full email address.
+ :email: "gitlab-incoming@gmail.com"
+ # Email account password
+ :password: "[REDACTED]"
+ # The name of the mailbox where incoming mail will end up. Usually "inbox".
+ :name: "inbox"
+ # Always "sidekiq".
+ :delivery_method: sidekiq
+ # Always true.
+ :delete_after_delivery: true
+ :delivery_options:
+ # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml.
+ :redis_url: redis://localhost:6379
+ # Always "resque:gitlab".
+ :namespace: resque:gitlab
+ # Always "incoming_email".
+ :queue: incoming_email
+ # Always "EmailReceiverWorker"
+ :worker: EmailReceiverWorker
+ ```
+
+4. Uncomment the `mail_room` line in your `Procfile`:
+
+ ```yaml
+ mail_room: bundle exec mail_room -q -c config/mail_room.yml
+ ```
+
+6. Restart GitLab:
+
+ ```sh
+ bundle exec foreman start
+ ```
+
+7. Verify that everything is configured correctly:
+
+ ```sh
+ bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
+ ```
+
+8. Reply by email should now be working. \ No newline at end of file
diff --git a/doc/incoming_email/postfix.md b/doc/incoming_email/postfix.md
new file mode 100644
index 00000000000..18bf3db1744
--- /dev/null
+++ b/doc/incoming_email/postfix.md
@@ -0,0 +1,310 @@
+# Set up Postfix for Reply by email
+
+This document will take you through the steps of setting up a basic Postfix mail server with IMAP authentication on Ubuntu, to be used with Reply by email.
+
+The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
+
+## Configure your server firewall
+
+1. Open up port 25 on your server so that people can send email into the server over SMTP.
+2. If the mail server is different from the server running GitLab, open up port 143 on your server so that GitLab can read email from the server over IMAP.
+
+## Install packages
+
+1. Install the `postfix` package if it is not installed already:
+
+ ```sh
+ sudo apt-get install postfix
+ ```
+
+ When asked about the environment, select 'Internet Site'. When asked to confirm the hostname, make sure it matches `gitlab.example.com`.
+
+1. Install the `mailutils` package.
+
+ ```sh
+ sudo apt-get install mailutils
+ ```
+
+## Create user
+
+1. Create a user for incoming email.
+
+ ```sh
+ sudo useradd -m -s /bin/bash incoming
+ ```
+
+1. Set a password for this user.
+
+ ```sh
+ sudo passwd incoming
+ ```
+
+ Be sure not to forget this, you'll need it later.
+
+## Test the out-of-the-box setup
+
+1. Connect to the local SMTP server:
+
+ ```sh
+ telnet localhost 25
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 127.0.0.1...
+ Connected to localhost.
+ Escape character is '^]'.
+ 220 gitlab.example.com ESMTP Postfix (Ubuntu)
+ ```
+
+ If you get a `Connection refused` error instead, verify that `postfix` is running:
+
+ ```sh
+ sudo postfix status
+ ```
+
+ If it is not, start it:
+
+ ```sh
+ sudo postfix start
+ ```
+
+1. Send the new `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+ ```
+ ehlo localhost
+ mail from: root@localhost
+ rcpt to: incoming@localhost
+ data
+ Subject: Re: Some issue
+
+ Sounds good!
+ .
+ quit
+ ```
+
+ (Note: The `.` is a literal period on its own line)
+
+1. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/var/mail/incoming": 1 message 1 unread
+ >U 1 root@localhost 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+## Configure Postfix to use Maildir-style mailboxes
+
+Courier, which we will install later to add IMAP authentication, requires mailboxes to have the Maildir format, rather than mbox.
+
+1. Configure Postfix to use Maildir-style mailboxes:
+
+ ```sh
+ sudo postconf -e "home_mailbox = Maildir/"
+ ```
+
+1. Restart Postfix:
+
+ ```sh
+ sudo /etc/init.d/postfix restart
+ ```
+
+1. Test the new setup:
+
+ 1. Follow steps 1 and 2 of _[Test the out-of-the-box setup](#test-the-out-of-the-box-setup)_.
+ 2. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ MAIL=/home/incoming/Maildir
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/home/incoming/Maildir": 1 message 1 unread
+ >U 1 root@localhost 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+## Install the Courier IMAP server
+
+1. Install the `courier-imap` package:
+
+ ```sh
+ sudo apt-get install courier-imap
+ ```
+
+## Configure Postfix to receive email from the internet
+
+1. Let Postfix know about the domains that it should consider local:
+
+ ```sh
+ sudo postconf -e "mydestination = gitlab.example.com, localhost.localdomain, localhost"
+ ```
+
+1. Let Postfix know about the IPs that it should consider part of the LAN:
+
+ We'll assume `192.168.1.0/24` is your local LAN. You can safely skip this step if you don't have other machines in the same local network.
+
+ ```sh
+ sudo postconf -e "mynetworks = 127.0.0.0/8, 192.168.1.0/24"
+ ```
+
+1. Configure Postfix to receive mail on all interfaces, which includes the internet:
+
+ ```sh
+ sudo postconf -e "inet_interfaces = all"
+ ```
+
+1. Configure Postfix to use the `+` delimiter for sub-addressing:
+
+ ```sh
+ sudo postconf -e "recipient_delimiter = +"
+ ```
+
+1. Restart Postfix:
+
+ ```sh
+ sudo service postfix restart
+ ```
+
+## Test the final setup
+
+1. Test SMTP under the new setup:
+
+ 1. Connect to the SMTP server:
+
+ ```sh
+ telnet gitlab.example.com 25
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 123.123.123.123...
+ Connected to gitlab.example.com.
+ Escape character is '^]'.
+ 220 gitlab.example.com ESMTP Postfix (Ubuntu)
+ ```
+
+ If you get a `Connection refused` error instead, make sure your firewall is setup to allow inbound traffic on port 25.
+
+ 1. Send the `incoming` user a dummy email to test SMTP, by entering the following into the SMTP prompt:
+
+ ```
+ ehlo gitlab.example.com
+ mail from: root@gitlab.example.com
+ rcpt to: incoming@gitlab.example.com
+ data
+ Subject: Re: Some issue
+
+ Sounds good!
+ .
+ quit
+ ```
+
+ (Note: The `.` is a literal period on its own line)
+
+ 1. Check if the `incoming` user received the email:
+
+ ```sh
+ su - incoming
+ MAIL=/home/incoming/Maildir
+ mail
+ ```
+
+ You should see output like this:
+
+ ```
+ "/home/incoming/Maildir": 1 message 1 unread
+ >U 1 root@gitlab.example.com 59/2842 Re: Some issue
+ ```
+
+ Quit the mail app:
+
+ ```sh
+ q
+ ```
+
+ 1. Log out of the `incoming` account and go back to being `root`:
+
+ ```sh
+ logout
+ ```
+
+1. Test IMAP under the new setup:
+
+ 1. Connect to the IMAP server:
+
+ ```sh
+ telnet gitlab.example.com 143
+ ```
+
+ You should see a prompt like this:
+
+ ```sh
+ Trying 123.123.123.123...
+ Connected to mail.example.gitlab.com.
+ Escape character is '^]'.
+ - OK [CAPABILITY IMAP4rev1 UIDPLUS CHILDREN NAMESPACE THREAD=ORDEREDSUBJECT THREAD=REFERENCES SORT QUOTA IDLE ACL ACL2=UNION] Courier-IMAP ready. Copyright 1998-2011 Double Precision, Inc. See COPYING for distribution information.
+ ```
+
+ 1. Sign in as the `incoming` user to test IMAP, by entering the following into the IMAP prompt:
+
+ ```
+ a login incoming PASSWORD
+ ```
+
+ Replace PASSWORD with the password you set on the `incoming` user earlier.
+
+ You should see output like this:
+
+ ```
+ a OK LOGIN Ok.
+ ```
+
+ 1. Disconnect from the IMAP server:
+
+ ```sh
+ a logout
+ ```
+
+## Done!
+
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./README.md) guide to configure GitLab.
+
+---------
+
+_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index 362c492d0ac..c565e90da2f 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -36,7 +36,7 @@ We do not recommend using MySQL due to various issues. For example, case [(in)se
mysql> CREATE DATABASE IF NOT EXISTS `gitlabhq_production` DEFAULT CHARACTER SET `utf8` COLLATE `utf8_unicode_ci`;
# Grant the GitLab user necessary permissions on the database
- mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost';
+ mysql> GRANT SELECT, INSERT, UPDATE, DELETE, CREATE, CREATE TEMPORARY TABLES, DROP, INDEX, ALTER, LOCK TABLES ON `gitlabhq_production`.* TO 'git'@'localhost';
# Quit the database session
mysql> \q
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 52031e9b9a1..3c62b11988e 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -115,8 +115,9 @@ Remove the old Ruby 1.8 if present
Download Ruby and compile it:
mkdir /tmp/ruby && cd /tmp/ruby
- curl -L --progress http://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.6.tar.gz | tar xz
- cd ruby-2.1.6
+ curl -O --progress https://cache.ruby-lang.org/pub/ruby/2.1/ruby-2.1.7.tar.gz
+ echo 'e2e195a4a58133e3ad33b955c829bb536fa3c075 ruby-2.1.7.tar.gz' | shasum -c - && tar xzf ruby-2.1.7.tar.gz
+ cd ruby-2.1.7
./configure --disable-install-rdoc
make
sudo make install
@@ -130,12 +131,15 @@ Install the Bundler Gem:
Since GitLab 8.0, Git HTTP requests are handled by gitlab-git-http-server.
This is a small daemon written in Go.
To install gitlab-git-http-server we need a Go compiler.
+The instructions below assume you use 64-bit Linux. You can find
+downloads for other platforms at the [Go download
+page](https://golang.org/dl).
- curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
- echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \
- sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz
+ curl -O --progress https://storage.googleapis.com/golang/go1.5.1.linux-amd64.tar.gz
+ echo '46eecd290d8803887dec718c691cc243f2175fe0 go1.5.1.linux-amd64.tar.gz' | shasum -c - && \
+ sudo tar -C /usr/local -xzf go1.5.1.linux-amd64.tar.gz
sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
- rm go1.5.linux-amd64.tar.gz
+ rm go1.5.1.linux-amd64.tar.gz
## 4. System Users
@@ -207,9 +211,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 7-14-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-0-stable gitlab
-**Note:** You can change `7-14-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-0-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -222,6 +226,10 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Update GitLab config file, follow the directions at top of file
sudo -u git -H editor config/gitlab.yml
+ # Copy the example secrets file
+ sudo -u git -H cp config/secrets.yml.example config/secrets.yml
+ sudo -u git -H chmod 0600 config/secrets.yml
+
# Make sure GitLab can write to the log/ and tmp/ directories
sudo chown -R git log/
sudo chown -R git tmp/
@@ -235,6 +243,9 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
# Make sure GitLab can write to the public/uploads/ directory
sudo chmod -R u+rwX public/uploads
+ # Change the permissions of the directory where CI build traces are stored
+ sudo chmod -R u+rwX builds/
+
# Copy the example Unicorn config
sudo -u git -H cp config/unicorn.rb.example config/unicorn.rb
@@ -299,7 +310,7 @@ We recommend using a PostgreSQL database. For MySQL check [MySQL setup guide](da
GitLab Shell is an SSH access and repository management software developed specially for GitLab.
# Run the installation task for gitlab-shell (replace `REDIS_URL` if needed):
- sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.3] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:shell:install[v2.6.5] REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
# By default, the gitlab-shell config is generated from your main GitLab config.
# You can review (and modify) the gitlab-shell config as follows:
@@ -328,6 +339,17 @@ GitLab Shell is an SSH access and repository management software developed speci
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword
+### 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.
+
+### Install schedules
+
+ # Setup schedules
+ sudo -u gitlab_ci -H bundle exec whenever -w RAILS_ENV=production
+
### Install Init Script
Download the init script (will be `/etc/init.d/gitlab`):
@@ -456,9 +478,22 @@ Using a self-signed certificate is discouraged but if you must use it follow the
```
1. In the `config.yml` of gitlab-shell set `self_signed_cert` to `true`.
-### Additional Markup Styles
+### Enable Reply by email
-Apart from the always supported markdown style there are other rich text files that GitLab can display. But you might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information.
+See the ["Reply by email" documentation](../incoming_email/README.md) for more information on how to set this up.
+
+### LDAP Authentication
+
+You can configure LDAP authentication in `config/gitlab.yml`. Please restart GitLab after editing this file.
+
+### Using Custom Omniauth Providers
+
+See the [omniauth integration document](../integration/omniauth.md)
+
+### Build your projects
+
+GitLab can build your projects. To enable that feature you need GitLab Runners to do that for you.
+Checkout the [Gitlab Runner section](https://about.gitlab.com/gitlab-ci/#gitlab-runner) to install it
### Custom Redis Connection
@@ -484,10 +519,16 @@ If you are running SSH on a non-standard port, you must change the GitLab user's
You also need to change the corresponding options (e.g. `ssh_user`, `ssh_host`, `admin_uri`) in the `config\gitlab.yml` file.
-### LDAP Authentication
+### Additional Markup Styles
-You can configure LDAP authentication in `config/gitlab.yml`. Please restart GitLab after editing this file.
+Apart from the always supported markdown style there are other rich text files that GitLab can display. But you might have to install a dependency to do so. Please see the [github-markup gem readme](https://github.com/gitlabhq/markup#markups) for more information.
-### Using Custom Omniauth Providers
+## Troubleshooting
-See the [omniauth integration document](../integration/omniauth.md)
+### "You appear to have cloned an empty repository."
+
+If you see this message when attempting to clone a repository hosted by GitLab,
+this is likely due to an outdated Nginx or Apache configuration, or a missing or
+misconfigured `gitlab-git-http-server` instance. Double-check that you've
+[installed Go](#3-go), [installed gitlab-git-http-server](#install-gitlab-git-http-server),
+and correctly [configured Nginx](#site-configuration).
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 6d856951d4e..eff39a626ae 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -10,7 +10,7 @@ See the documentation below for details on how to configure these services.
- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
- [Slack](slack.md) Integrate with the Slack chat service
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
-- [Gmail](gitlab_buttons_in_gmail.md) Adds GitLab actions to messages
+- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
GitLab Enterprise Edition contains [advanced JIRA support](http://doc.gitlab.com/ee/integration/jira.html) and [advanced Jenkins support](http://doc.gitlab.com/ee/integration/jenkins.html).
diff --git a/doc/integration/crowd.md b/doc/integration/crowd.md
new file mode 100644
index 00000000000..2ecc8795ac1
--- /dev/null
+++ b/doc/integration/crowd.md
@@ -0,0 +1,58 @@
+# Crowd OmniAuth Provider
+
+To enable the Crowd OmniAuth provider you must register your application with Crowd. To configure Crowd integration you need an application name and password.
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For instalations from source:
+
+ ```sh
+ cd /home/git/gitlab
+
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "crowd",
+ "args" => {
+ "crowd_server_url" => "CROWD",
+ "application_name" => "YOUR_APP_NAME",
+ "application_password" => "YOUR_APP_PASSWORD"
+ }
+ }
+ ]
+ ```
+
+ For installations from source:
+
+ ```
+ - { name: 'crowd',
+ args: {
+ crowd_server_url: 'CROWD SERVER URL',
+ application_name: 'YOUR_APP_NAME',
+ application_password: 'YOUR_APP_PASSWORD' } }
+ ```
+
+1. Change 'YOUR_APP_NAME' to the application name from Crowd applications page.
+
+1. Change 'YOUR_APP_PASSWORD' to the application password you've set.
+
+1. Save the configuration file.
+
+1. Restart GitLab for the changes to take effect.
+
+On the sign in page there should now be a Crowd tab in the sign in form. \ No newline at end of file
diff --git a/doc/integration/gmail_action_buttons_for_gitlab.md b/doc/integration/gmail_action_buttons_for_gitlab.md
new file mode 100644
index 00000000000..de45f25ad62
--- /dev/null
+++ b/doc/integration/gmail_action_buttons_for_gitlab.md
@@ -0,0 +1,22 @@
+# Gmail actions buttons for GitLab
+
+GitLab supports [Google actions in email](https://developers.google.com/gmail/markup/actions/actions-overview).
+
+If correctly setup, emails that require an action will be marked in Gmail.
+
+![gmail_actions_button.png](gmail_actions_button.png)
+
+To get this functioning, you need to be registered with Google.
+[See how to register with Google in this document.](https://developers.google.com/gmail/markup/registering-with-google)
+
+*This process has a lot of steps so make sure that you fulfill all requirements set by Google.*
+*Your application will be rejected by Google if you fail to do so.*
+
+Pay close attention to:
+
+* Email account used by GitLab to send notification emails needs to have "Consistent history of sending a high volume of mail from your domain (order of hundred emails a day minimum to Gmail) for a few weeks at least".
+* "A very very low rate of spam complaints from users."
+* Emails must be authenticated via DKIM or SPF
+* Before sending the final form("Gmail Schema Whitelist Request"), you must send a real email from your production server. This means that you will have to find a way to send this email from the email address you are registering. You can do this by, for example, forwarding the real email from the email address you are registering or going into the rails console on the GitLab server and triggering the email sending from there.
+
+You can check how it looks going through all the steps laid out in the "Registering with Google" doc in [this GitLab.com issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/1517).
diff --git a/doc/integration/gmail_actions_button.png b/doc/integration/gmail_actions_button.png
new file mode 100644
index 00000000000..b08f54d137b
--- /dev/null
+++ b/doc/integration/gmail_actions_button.png
Binary files differ
diff --git a/doc/integration/ldap.md b/doc/integration/ldap.md
index 904d5d7fee2..9b7d8fa3969 100644
--- a/doc/integration/ldap.md
+++ b/doc/integration/ldap.md
@@ -78,6 +78,26 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
#
user_filter: ''
+ # LDAP attributes that GitLab will use to create an account for the LDAP user.
+ # The specified attribute can either be the attribute name as a string (e.g. 'mail'),
+ # or an array of attribute names to try in order (e.g. ['mail', 'email']).
+ # Note that the user's LDAP login will always be the attribute specified as `uid` above.
+ attributes:
+ # The username will be used in paths for the user's own projects
+ # (like `gitlab.example.com/username/project`) and when mentioning
+ # them in issues, merge request and comments (like `@username`).
+ # If the attribute specified for `username` contains an email address,
+ # the GitLab username will be the part of the email address before the '@'.
+ username: ['uid', 'userid', 'sAMAccountName']
+ email: ['mail', 'email', 'userPrincipalName']
+
+ # If no full name could be found at the attribute specified for `name`,
+ # the full name is determined using the attributes specified for
+ # `first_name` and `last_name`.
+ name: 'cn'
+ first_name: 'givenName'
+ last_name: 'sn'
+
# GitLab EE only: add more LDAP servers
# Choose an ID made of a-z and 0-9 . This ID will be stored in the database
# so that GitLab can remember which LDAP server a user belongs to.
@@ -153,3 +173,23 @@ Tip: if you want to limit access to the nested members of an Active Directory gr
```
Please note that GitLab does not support the custom filter syntax used by omniauth-ldap.
+
+## Limitations
+
+GitLab's LDAP client is based on [omniauth-ldap](https://gitlab.com/gitlab-org/omniauth-ldap)
+which encapsulates Ruby's `Net::LDAP` class. It provides a pure-Ruby implementation
+of the LDAP client protocol. As a result, GitLab is limited by `omniauth-ldap` and may impact your LDAP
+server settings.
+
+### TLS Client Authentication
+Not implemented by `Net::LDAP`.
+So you should disable anonymous LDAP authentication and enable simple or SASL
+authentication. TLS client authentication setting in your LDAP server cannot be
+mandatory and clients cannot be authenticated with the TLS protocol.
+
+### TLS Server Authentication
+Not supported by GitLab's configuration options.
+When setting `method: ssl`, the underlying authentication method used by
+`omniauth-ldap` is `simple_tls`. This method establishes TLS encryption with
+the LDAP server before any LDAP-protocol data is exchanged but no validation of
+the LDAP server's SSL certificate is performed. \ No newline at end of file
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 2010cb9b8a1..c5cecbc2f2d 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -76,6 +76,7 @@ Now we can choose one or more of the Supported Providers below to continue confi
- [Shibboleth](shibboleth.md)
- [Twitter](twitter.md)
- [SAML](saml.md)
+- [Crowd](crowd.md)
## Enable OmniAuth for an Existing User
diff --git a/doc/markdown/markdown.md b/doc/markdown/markdown.md
index 322111ae9e1..ac3851f8c95 100644
--- a/doc/markdown/markdown.md
+++ b/doc/markdown/markdown.md
@@ -33,7 +33,6 @@ For GitLab we developed something we call "GitLab Flavored Markdown" (GFM). It e
You can use GFM in
-- commit messages
- comments
- issues
- merge requests
@@ -275,7 +274,7 @@ The IDs are generated from the content of the header according to the following
1. All spaces are converted to hyphens
1. Two or more hyphens in a row are converted to one
1. If a header with the same ID has already been generated, a unique
- incrementing number is appended.
+ incrementing number is appended, starting at 1.
For example:
@@ -292,8 +291,8 @@ Would generate the following link IDs:
1. `this-header-has-spaces-in-it`
1. `this-header-has-a-in-it`
1. `this-header-has-unicode-in-it-한글`
+1. `this-header-has-spaces-in-it`
1. `this-header-has-spaces-in-it-1`
-1. `this-header-has-spaces-in-it-2`
Note that the Emoji processing happens before the header IDs are generated, so the Emoji is converted to an image which then gets removed from the ID.
@@ -413,7 +412,7 @@ Some text to show that the reference links can follow later.
Relative links do not allow referencing project files in a wiki page or wiki page in a project file. The reason for this is that, in GitLab, wiki is always a separate git repository. For example:
-`[I'm a reference-style link][style]`
+`[I'm a reference-style link](style)`
will point the link to `wikis/style` when the link is inside of a wiki markdown file.
diff --git a/doc/migrate_ci_to_ce/README.md b/doc/migrate_ci_to_ce/README.md
new file mode 100644
index 00000000000..5ec0a2069b5
--- /dev/null
+++ b/doc/migrate_ci_to_ce/README.md
@@ -0,0 +1,435 @@
+## Migrate GitLab CI to GitLab CE or EE
+
+Beginning with version 8.0 of GitLab Community Edition (CE) and Enterprise
+Edition (EE), GitLab CI is no longer its own application, but is instead built
+into the CE and EE applications.
+
+This guide will detail the process of migrating your CI installation and data
+into your GitLab CE or EE installation. **You can only migrate CI data from
+GitLab CI 8.0 to GitLab 8.0; migrating between other versions (e.g.7.14 to 8.1)
+is not possible.**
+
+We recommend that you read through the entire migration process in this
+document before beginning.
+
+### Overview
+
+In this document we assume you have a GitLab server and a GitLab CI server. It
+does not matter if these are the same machine.
+
+The migration consists of three parts: updating GitLab and GitLab CI, moving
+data, and redirecting traffic.
+
+Please note that CI builds triggered on your GitLab server in the time between
+updating to 8.0 and finishing the migration will be lost. Your GitLab server
+can be online for most of the procedure; the only GitLab downtime (if any) is
+during the upgrade to 8.0. Your CI service will be offline from the moment you
+upgrade to 8.0 until you finish the migration procedure.
+
+### Before upgrading
+
+If you have GitLab CI installed using omnibus-gitlab packages but **you don't want to migrate your existing data**:
+
+```bash
+mv /var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds.$(date +%s)
+```
+
+run `sudo gitlab-ctl reconfigure` and you can reach CI at `gitlab.example.com/ci`.
+
+If you want to migrate your existing data, continue reading.
+
+#### 0. Updating Omnibus from versions prior to 7.13
+
+If you are updating from older versions you should first update to 7.14 and then to 8.0.
+Otherwise it's pretty likely that you will encounter problems described in the [Troubleshooting](#troubleshooting).
+
+#### 1. Verify that backups work
+
+Make sure that the backup script on both servers can connect to the database.
+
+```
+# On your CI server:
+# Omnibus
+sudo chown gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
+sudo gitlab-ci-rake backup:create
+
+# Source
+cd /home/gitlab_ci/gitlab-ci
+sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production
+```
+
+Also check on your GitLab server.
+
+```
+# On your GitLab server:
+# Omnibus
+sudo gitlab-rake gitlab:backup:create SKIP=repositories,uploads
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production SKIP=repositories,uploads
+```
+
+If this fails you need to fix it before upgrading to 8.0. Also see
+https://about.gitlab.com/getting-help/
+
+#### 2. Check source and target database types
+
+Check what databases you use on your GitLab server and your CI server.
+ Look for the 'adapter:' line. If your CI server and your GitLab server use
+the same database adapter no special care is needed. If your CI server uses
+MySQL and your GitLab server uses PostgreSQL you need to pass a special option
+during the 'Moving data' part. **If your CI server uses PostgreSQL and your
+GitLab server uses MySQL you cannot migrate your CI data to GitLab 8.0.**
+
+```
+# On your CI server:
+# Omnibus
+sudo gitlab-ci-rake env:info
+
+# Source
+cd /home/gitlab_ci/gitlab-ci
+sudo -u gitlab_ci -H bundle exec rake env:info RAILS_ENV=production
+```
+
+```
+# On your GitLab server:
+# Omnibus
+sudo gitlab-rake gitlab:env:info
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+```
+
+#### 3. Storage planning
+
+Decide where to store CI build traces on GitLab server. GitLab CI uses
+ files on disk to store CI build traces. The default path for these build
+traces is `/var/opt/gitlab/gitlab-ci/builds` (Omnibus) or
+`/home/git/gitlab/builds` (Source). If you are storing your repository data in
+a special location, or if you are using NFS, you should make sure that you
+store build traces on the same storage as your Git repositories.
+
+### I. Upgrading
+
+From this point on, GitLab CI will be unavailable for your end users.
+
+#### 1. Upgrade GitLab to 8.0
+
+First upgrade your GitLab server to version 8.0:
+https://about.gitlab.com/update/
+
+#### 2. Disable CI on the GitLab server during the migration
+
+After you update, go to the admin panel and temporarily disable CI. As
+ an administrator, go to **Admin Area** -> **Settings**, and under
+**Continuous Integration** uncheck **Disable to prevent CI usage until rake
+ci:migrate is run (8.0 only)**.
+
+#### 3. CI settings are now in GitLab
+
+If you want to use custom CI settings (e.g. change where builds are
+ stored), please update `/etc/gitlab/gitlab.rb` (Omnibus) or
+`/home/git/gitlab/config/gitlab.yml` (Source).
+
+#### 4. Upgrade GitLab CI to 8.0
+
+Now upgrade GitLab CI to version 8.0. If you are using Omnibus packages,
+ this may have already happened when you upgraded GitLab to 8.0.
+
+#### 5. Disable GitLab CI on the CI server
+
+Disable GitLab CI after upgrading to 8.0.
+
+```
+# On your CI server:
+# Omnibus
+sudo gitlab-ctl stop ci-unicorn
+sudo gitlab-ctl stop ci-sidekiq
+
+# Source
+sudo service gitlab_ci stop
+cd /home/gitlab_ci/gitlab-ci
+sudo -u gitlab_ci -H bundle exec whenever --clear-crontab RAILS_ENV=production
+```
+
+### II. Moving data
+
+#### 1. Database encryption key
+
+Move the database encryption key from your CI server to your GitLab
+ server. The command below will show you what you need to copy-paste to your
+GitLab server. On Omnibus GitLab servers you will have to add a line to
+`/etc/gitlab/gitlab.rb`. On GitLab servers installed from source you will have
+to replace the contents of `/home/git/gitlab/config/secrets.yml`.
+
+```
+# On your CI server:
+# Omnibus
+sudo gitlab-ci-rake backup:show_secrets
+
+# Source
+cd /home/gitlab_ci/gitlab-ci
+sudo -u gitlab_ci -H bundle exec rake backup:show_secrets RAILS_ENV=production
+```
+
+#### 2. SQL data and build traces
+
+Create your final CI data export. If you are converting from MySQL to
+ PostgreSQL, add ` MYSQL_TO_POSTGRESQL=1` to the end of the rake command. When
+the command finishes it will print the path to your data export archive; you
+will need this file later.
+
+```
+# On your CI server:
+# Omnibus
+sudo chown gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
+sudo gitlab-ci-rake backup:create
+
+# Source
+cd /home/gitlab_ci/gitlab-ci
+sudo -u gitlab_ci -H bundle exec rake backup:create RAILS_ENV=production
+```
+
+#### 3. Copy data to the GitLab server
+
+If you were running GitLab and GitLab CI on the same server you can skip this
+step.
+
+Copy your CI data archive to your GitLab server. There are many ways to do
+this, below we use SSH agent forwarding and 'scp', which will be easy and fast
+for most setups. You can also copy the data archive first from the CI server to
+your laptop and then from your laptop to the GitLab server.
+
+```
+# Start from your laptop
+ssh -A ci_admin@ci_server.example
+# Now on the CI server
+scp /path/to/12345_gitlab_ci_backup.tar gitlab_admin@gitlab_server.example:~
+```
+
+#### 4. Move data to the GitLab backups folder
+
+Make the CI data archive discoverable for GitLab. We assume below that you
+store backups in the default path, adjust the command if necessary.
+
+```
+# On your GitLab server:
+# Omnibus
+sudo mv /path/to/12345_gitlab_ci_backup.tar /var/opt/gitlab/backups/
+
+# Source
+sudo mv /path/to/12345_gitlab_ci_backup.tar /home/git/gitlab/tmp/backups/
+```
+
+#### 5. Import the CI data into GitLab.
+
+This step will delete any existing CI data on your GitLab server. There should
+be no CI data yet because you turned CI on the GitLab server off earlier.
+
+```
+# On your GitLab server:
+# Omnibus
+sudo chown git:git /var/opt/gitlab/gitlab-ci/builds
+sudo gitlab-rake ci:migrate
+
+# Source
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake ci:migrate RAILS_ENV=production
+```
+
+#### 6. Restart GitLab
+
+```
+# On your GitLab server:
+# Omnibus
+sudo gitlab-ctl hup unicorn
+sudo gitlab-ctl restart sidekiq
+
+# Source
+sudo service gitlab reload
+```
+
+### III. Redirecting traffic
+
+If you were running GitLab CI with Omnibus packages and you were using the
+internal NGINX configuration your CI service should now be available both at
+`ci.example.com` (the old address) and `gitlab.example.com/ci`. **You are done!**
+
+If you installed GitLab CI from source we now need to configure a redirect in
+NGINX so that existing CI runners can keep using the old CI server address, and
+so that existing links to your CI server keep working.
+
+#### 1. Update Nginx configuration
+
+To ensure that your existing CI runners are able to communicate with the
+migrated installation, and that existing build triggers still work, you'll need
+to update your Nginx configuration to redirect requests for the old locations to
+the new ones.
+
+Edit `/etc/nginx/sites-available/gitlab_ci` and paste:
+
+```nginx
+# GITLAB CI
+server {
+ listen 80 default_server; # e.g., listen 192.168.1.1:80;
+ server_name YOUR_CI_SERVER_FQDN; # e.g., server_name source.example.com;
+
+ access_log /var/log/nginx/gitlab_ci_access.log;
+ error_log /var/log/nginx/gitlab_ci_error.log;
+
+ # expose API to fix runners
+ location /api {
+ proxy_read_timeout 300;
+ proxy_connect_timeout 300;
+ proxy_redirect off;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN
+ resolver 8.8.8.8 8.8.4.4;
+ proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # redirect all other CI requests
+ location / {
+ return 301 $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # adjust this to match the largest build log your runners might submit,
+ # set to 0 to disable limit
+ client_max_body_size 10m;
+}
+```
+
+Make sure you substitute these placeholder values with your real ones:
+
+1. `YOUR_CI_SERVER_FQDN`: The existing public-facing address of your GitLab CI
+ install (e.g., `ci.gitlab.com`).
+1. `YOUR_GITLAB_SERVER_FQDN`: The current public-facing address of your GitLab
+ CE (or EE) install (e.g., `gitlab.com`).
+
+**Make sure not to remove the `/ci$request_uri` part. This is required to
+properly forward the requests.**
+
+You should also make sure that you can:
+
+1. `curl https://YOUR_GITLAB_SERVER_FQDN/` from your previous GitLab CI server.
+1. `curl https://YOUR_CI_SERVER_FQDN/` from your GitLab CE (or EE) server.
+
+#### 2. Check Nginx configuration
+
+ sudo nginx -t
+
+#### 3. Restart Nginx
+
+ sudo /etc/init.d/nginx restart
+
+#### Restore from backup
+
+If something went wrong and you need to restore a backup, consult the [Backup
+restoration](../raketasks/backup_restore.md) guide.
+
+### Troubleshooting
+
+#### show:secrets problem (Omnibus-only)
+If you see errors like this:
+```
+Missing `secret_key_base` or `db_key_base` for 'production' environment. The secrets will be generated and stored in `config/secrets.yml`
+rake aborted!
+Errno::EACCES: Permission denied @ rb_sysopen - config/secrets.yml
+```
+
+This can happen if you are updating from versions prior to 7.13 straight to 8.0.
+The fix for this is to update to Omnibus 7.14 first and then update it to 8.0.
+
+#### Permission denied when accessing /var/opt/gitlab/gitlab-ci/builds
+To fix that issue you have to change builds/ folder permission before doing final backup:
+```
+sudo chown -R gitlab-ci:gitlab-ci /var/opt/gitlab/gitlab-ci/builds
+```
+
+Then before executing `ci:migrate` you need to fix builds folder permission:
+```
+sudo chown git:git /var/opt/gitlab/gitlab-ci/builds
+```
+
+#### Problems when importing CI database to GitLab
+If you were migrating CI database from MySQL to PostgreSQL manually you can see errros during import about missing sequences:
+```
+ALTER SEQUENCE
+ERROR: relation "ci_builds_id_seq" does not exist
+ERROR: relation "ci_commits_id_seq" does not exist
+ERROR: relation "ci_events_id_seq" does not exist
+ERROR: relation "ci_jobs_id_seq" does not exist
+ERROR: relation "ci_projects_id_seq" does not exist
+ERROR: relation "ci_runner_projects_id_seq" does not exist
+ERROR: relation "ci_runners_id_seq" does not exist
+ERROR: relation "ci_services_id_seq" does not exist
+ERROR: relation "ci_taggings_id_seq" does not exist
+ERROR: relation "ci_tags_id_seq" does not exist
+CREATE TABLE
+```
+
+To fix that you need to apply this SQL statement before doing final backup:
+```
+# Omnibus
+gitlab-ci-rails dbconsole <<EOF
+-- ALTER TABLES - DROP DEFAULTS
+ALTER TABLE ONLY ci_application_settings ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_builds ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_commits ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_events ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_jobs ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_projects ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_runner_projects ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_runners ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_services ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_taggings ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_tags ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_trigger_requests ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_triggers ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_variables ALTER COLUMN id DROP DEFAULT;
+ALTER TABLE ONLY ci_web_hooks ALTER COLUMN id DROP DEFAULT;
+
+-- ALTER SEQUENCES
+ALTER SEQUENCE ci_application_settings_id_seq OWNED BY ci_application_settings.id;
+ALTER SEQUENCE ci_builds_id_seq OWNED BY ci_builds.id;
+ALTER SEQUENCE ci_commits_id_seq OWNED BY ci_commits.id;
+ALTER SEQUENCE ci_events_id_seq OWNED BY ci_events.id;
+ALTER SEQUENCE ci_jobs_id_seq OWNED BY ci_jobs.id;
+ALTER SEQUENCE ci_projects_id_seq OWNED BY ci_projects.id;
+ALTER SEQUENCE ci_runner_projects_id_seq OWNED BY ci_runner_projects.id;
+ALTER SEQUENCE ci_runners_id_seq OWNED BY ci_runners.id;
+ALTER SEQUENCE ci_services_id_seq OWNED BY ci_services.id;
+ALTER SEQUENCE ci_taggings_id_seq OWNED BY ci_taggings.id;
+ALTER SEQUENCE ci_tags_id_seq OWNED BY ci_tags.id;
+ALTER SEQUENCE ci_trigger_requests_id_seq OWNED BY ci_trigger_requests.id;
+ALTER SEQUENCE ci_triggers_id_seq OWNED BY ci_triggers.id;
+ALTER SEQUENCE ci_variables_id_seq OWNED BY ci_variables.id;
+ALTER SEQUENCE ci_web_hooks_id_seq OWNED BY ci_web_hooks.id;
+
+-- ALTER TABLES - RE-APPLY DEFAULTS
+ALTER TABLE ONLY ci_application_settings ALTER COLUMN id SET DEFAULT nextval('ci_application_settings_id_seq'::regclass);
+ALTER TABLE ONLY ci_builds ALTER COLUMN id SET DEFAULT nextval('ci_builds_id_seq'::regclass);
+ALTER TABLE ONLY ci_commits ALTER COLUMN id SET DEFAULT nextval('ci_commits_id_seq'::regclass);
+ALTER TABLE ONLY ci_events ALTER COLUMN id SET DEFAULT nextval('ci_events_id_seq'::regclass);
+ALTER TABLE ONLY ci_jobs ALTER COLUMN id SET DEFAULT nextval('ci_jobs_id_seq'::regclass);
+ALTER TABLE ONLY ci_projects ALTER COLUMN id SET DEFAULT nextval('ci_projects_id_seq'::regclass);
+ALTER TABLE ONLY ci_runner_projects ALTER COLUMN id SET DEFAULT nextval('ci_runner_projects_id_seq'::regclass);
+ALTER TABLE ONLY ci_runners ALTER COLUMN id SET DEFAULT nextval('ci_runners_id_seq'::regclass);
+ALTER TABLE ONLY ci_services ALTER COLUMN id SET DEFAULT nextval('ci_services_id_seq'::regclass);
+ALTER TABLE ONLY ci_taggings ALTER COLUMN id SET DEFAULT nextval('ci_taggings_id_seq'::regclass);
+ALTER TABLE ONLY ci_tags ALTER COLUMN id SET DEFAULT nextval('ci_tags_id_seq'::regclass);
+ALTER TABLE ONLY ci_trigger_requests ALTER COLUMN id SET DEFAULT nextval('ci_trigger_requests_id_seq'::regclass);
+ALTER TABLE ONLY ci_triggers ALTER COLUMN id SET DEFAULT nextval('ci_triggers_id_seq'::regclass);
+ALTER TABLE ONLY ci_variables ALTER COLUMN id SET DEFAULT nextval('ci_variables_id_seq'::regclass);
+ALTER TABLE ONLY ci_web_hooks ALTER COLUMN id SET DEFAULT nextval('ci_web_hooks_id_seq'::regclass);
+EOF
+
+# Source
+cd /home/gitlab_ci/gitlab-ci
+sudo -u gitlab_ci -H bundle exec rails dbconsole production <<EOF
+... COPY SQL STATEMENTS FROM ABOVE ...
+EOF
+```
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 6a68c8e8286..db3f6bb40bd 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -7,7 +7,9 @@
A backup creates an archive file that contains the database, all repositories and all attachments.
This archive will be saved in backup_path (see `config/gitlab.yml`).
The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup.
-You can only restore a backup to exactly the same version of GitLab that you created it on, for example 7.2.1. The best way to migrate your repositories from one server to another is through backup restore.
+You can only restore a backup to exactly the same version of GitLab that you created it
+on, for example 7.2.1. The best way to migrate your repositories from one server to
+another is through backup restore.
You need to keep a separate copy of `/etc/gitlab/gitlab-secrets.json`
(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
@@ -95,6 +97,8 @@ For installations from source:
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'
+ # Turns on AWS Server-Side Encryption with Amazon S3-Managed Keys for backups, this is optional
+ # encryption: 'AES256'
```
If you are uploading your backups to S3 you will probably want to create a new
@@ -370,3 +374,10 @@ For more information see similar questions on postgresql issue tracker[here](htt
## Note
This documentation is for GitLab CE.
We backup GitLab.com and make sure your data is secure, but you can't use these methods to export / backup your data yourself from GitLab.com.
+
+Issues are stored in the database. They can't be stored in Git itself.
+
+To migrate your repositories from one server to another with an up-to-date version of
+GitLab, you can use the [import rake task](import.md) to do a mass import of the
+repository. Note that if you do an import rake task, rather than a backup restore, you
+will have all your repositories, but not any other data.
diff --git a/doc/raketasks/cleanup.md b/doc/raketasks/cleanup.md
index 96d67f7b5d6..8fbcbb983e9 100644
--- a/doc/raketasks/cleanup.md
+++ b/doc/raketasks/cleanup.md
@@ -12,7 +12,8 @@ sudo gitlab-rake gitlab:cleanup:dirs
bundle exec rake gitlab:cleanup:dirs RAILS_ENV=production
```
-Remove repositories (global only for now) from `/home/git/repositories` if they don't exist in GitLab database.
+Rename repositories from `/home/git/repositories` if they don't exist in GitLab database.
+The repositories get a `+orphaned+TIMESTAMP` suffix so that they cannot block new repositories from being created.
```
# omnibus-gitlab
diff --git a/doc/release/monthly.md b/doc/release/monthly.md
index c1ed9e3b80e..c56e99a7005 100644
--- a/doc/release/monthly.md
+++ b/doc/release/monthly.md
@@ -195,7 +195,7 @@ This can happen before tagging because Omnibus uses tags in its own repo and SHA
## Update GitLab.com with the stable version
- Deploy the package (should not need downtime because of the small difference with RC1)
-- Deploy the package for ci.gitlab.com
+- Deploy the package for gitlab.com/ci
## Release CE, EE and CI
diff --git a/doc/reply_by_email/README.md b/doc/reply_by_email/README.md
deleted file mode 100644
index 5d36f5121d1..00000000000
--- a/doc/reply_by_email/README.md
+++ /dev/null
@@ -1,180 +0,0 @@
-# Reply by email
-
-GitLab can be set up to allow users to comment on issues and merge requests by replying to notification emails.
-
-In order to do this, you need access to an IMAP-enabled email account, with a provider or server that supports [email sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing). Sub-addressing is a feature where any email to `user+some_arbitrary_tag@example.com` will end up in the mailbox for `user@example.com`, and is supported by providers such as Gmail, Yahoo! Mail, Outlook.com and iCloud, as well as the [Postfix](http://www.postfix.org/) mail server which you can run on-premises.
-
-## Set it up
-
-In this example, we'll use the Gmail address `gitlab-replies@gmail.com`. If you're actually using Gmail with Reply by email, make sure you have [IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018) and [allow less secure apps to access the account](https://support.google.com/accounts/answer/6010255).
-
-### Installations from source
-
-1. Go to the GitLab installation directory:
-
- ```sh
- cd /home/git/gitlab
- ```
-
-1. Find the `reply_by_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `reply_key`:
-
- ```sh
- sudo editor config/gitlab.yml
- ```
-
- ```yaml
- reply_by_email:
- enabled: true
- address: "gitlab-replies+%{reply_key}@gmail.com"
- ```
-
- As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-replies@gmail.com`.
-
-2. Find `config/mail_room.yml.example` and copy it to `config/mail_room.yml`:
-
- ```sh
- sudo cp config/mail_room.yml.example config/mail_room.yml
- ```
-
-3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account:
-
- ```sh
- sudo editor config/mail_room.yml
- ```
-
- ```yaml
- :mailboxes:
- -
- # IMAP server host
- :host: "imap.gmail.com"
- # IMAP server port
- :port: 993
- # Whether the IMAP server uses SSL
- :ssl: true
- # Email account username. Usually the full email address.
- :email: "gitlab-replies@gmail.com"
- # Email account password
- :password: "[REDACTED]"
- # The name of the mailbox where incoming mail will end up. Usually "inbox".
- :name: "inbox"
- # Always "sidekiq".
- :delivery_method: sidekiq
- # Always true.
- :delete_after_delivery: true
- :delivery_options:
- # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml.
- :redis_url: redis://localhost:6379
- # Always "resque:gitlab".
- :namespace: resque:gitlab
- # Always "incoming_email".
- :queue: incoming_email
- # Always "EmailReceiverWorker"
- :worker: EmailReceiverWorker
- ```
-
-
-4. Find `lib/support/init.d/gitlab.default.example` and copy it to `/etc/default/gitlab`:
-
- ```sh
- sudo cp lib/support/init.d/gitlab.default.example /etc/default/gitlab
- ```
-
-5. Edit `/etc/default/gitlab` to enable `mail_room`:
-
- ```sh
- sudo editor /etc/default/gitlab
- ```
-
- ```sh
- mail_room_enabled=true
- ```
-
-6. Restart GitLab:
-
- ```sh
- sudo service gitlab restart
- ```
-
-7. Check if everything is configured correctly:
-
- ```sh
- sudo bundle exec rake gitlab:reply_by_email:check RAILS_ENV=production
- ```
-
-8. Reply by email should now be working.
-
-### Omnibus package installations
-
-TODO
-
-### Development
-
-1. Go to the GitLab installation directory.
-
-1. Find the `reply_by_email` section in `config/gitlab.yml`, enable the feature and enter the email address including a placeholder for the `reply_key`:
-
- ```yaml
- reply_by_email:
- enabled: true
- address: "gitlab-replies+%{reply_key}@gmail.com"
- ```
-
- As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-replies@gmail.com`.
-
-2. Find `config/mail_room.yml.example` and copy it to `config/mail_room.yml`:
-
- ```sh
- sudo cp config/mail_room.yml.example config/mail_room.yml
- ```
-
-3. Uncomment the configuration options in `config/mail_room.yml` and fill in the details for your specific IMAP server and email account:
-
- ```yaml
- :mailboxes:
- -
- # IMAP server host
- :host: "imap.gmail.com"
- # IMAP server port
- :port: 993
- # Whether the IMAP server uses SSL
- :ssl: true
- # Email account username. Usually the full email address.
- :email: "gitlab-replies@gmail.com"
- # Email account password
- :password: "[REDACTED]"
- # The name of the mailbox where incoming mail will end up. Usually "inbox".
- :name: "inbox"
- # Always "sidekiq".
- :delivery_method: sidekiq
- # Always true.
- :delete_after_delivery: true
- :delivery_options:
- # The URL to the Redis server used by Sidekiq. Should match the URL in config/resque.yml.
- :redis_url: redis://localhost:6379
- # Always "resque:gitlab".
- :namespace: resque:gitlab
- # Always "incoming_email".
- :queue: incoming_email
- # Always "EmailReceiverWorker"
- :worker: EmailReceiverWorker
- ```
-
-4. Uncomment the `mail_room` line in your `Procfile`:
-
- ```yaml
- mail_room: bundle exec mail_room -q -c config/mail_room.yml
- ```
-
-6. Restart GitLab:
-
- ```sh
- bundle exec foreman start
- ```
-
-7. Check if everything is configured correctly:
-
- ```sh
- bundle exec rake gitlab:reply_by_email:check RAILS_ENV=development
- ```
-
-8. Reply by email should now be working.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 7cdcd11c04c..b6b8000af4e 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -3,27 +3,27 @@
## SSH keys
An SSH key allows you to establish a secure connection between your
-computer and GitLab.
-
-Before generating an SSH key, check if your system already has one by
-running `cat ~/.ssh/id_rsa.pub`. If you see a long string starting with
-`ssh-rsa` or `ssh-dsa`, you can skip the ssh-keygen step.
+computer and GitLab. Before generating an SSH key in your shell, check if your system
+already has one by running the following command:
+```bash
+cat ~/.ssh/id_rsa.pub
+```
-To generate a new SSH key, just open your terminal and use code below. The
-ssh-keygen command prompts you for a location and filename to store the key
-pair and for a password. When prompted for the location and filename, you
-can press enter to use the default.
+If you see a long string starting with `ssh-rsa` or `ssh-dsa`, you can skip the `ssh-keygen` step.
-It is a best practice to use a password for an SSH key, but it is not
+Note: It is a best practice to use a password for an SSH key, but it is not
required and you can skip creating a password by pressing enter. Note that
the password you choose here can't be altered or retrieved.
+To generate a new SSH key, use the following command:
```bash
ssh-keygen -t rsa -C "$your_email"
```
+This command will prompt you for a location and filename to store the key
+pair and for a password. When prompted for the location and filename, you
+can press enter to use the default.
-Use the code below to show your public key.
-
+Use the command below to show your public key:
```bash
cat ~/.ssh/id_rsa.pub
```
@@ -32,7 +32,7 @@ Copy-paste the key to the 'My SSH Keys' section under the 'SSH' tab in your
user profile. Please copy the complete key starting with `ssh-` and ending
with your username and host.
-Use code below to copy your public key to the clipboard. Depending on your
+To copy your public key to the clipboard, use code below. Depending on your
OS you'll need to use a different command:
**Windows:**
@@ -72,6 +72,8 @@ access can happen through being a direct member of the project, or through
a group. See `def accessible_deploy_keys` in `app/models/user.rb` for more
information.
+Deploy keys can be shared between projects, you just need to add them to each project.
+
## Applications
### Eclipse
diff --git a/doc/update/6.x-or-7.x-to-7.14.md b/doc/update/6.x-or-7.x-to-7.14.md
index a1488474f60..b34fb12da6f 100644
--- a/doc/update/6.x-or-7.x-to-7.14.md
+++ b/doc/update/6.x-or-7.x-to-7.14.md
@@ -127,7 +127,7 @@ sudo apt-get install nodejs
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch
-sudo -u git -H git checkout v2.6.4
+sudo -u git -H git checkout v2.6.5
```
## 7. Install libs, migrations, etc.
@@ -162,12 +162,12 @@ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
TIP: to see what changed in `gitlab.yml.example` in this release use next command:
```
-git diff 6-0-stable:config/gitlab.yml.example 7.14-stable:config/gitlab.yml.example
+git diff 6-0-stable:config/gitlab.yml.example 7-14-stable:config/gitlab.yml.example
```
* Make `/home/git/gitlab/config/gitlab.yml` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/gitlab.yml.example but with your settings.
* Make `/home/git/gitlab/config/unicorn.rb` the same as https://gitlab.com/gitlab-org/gitlab-ce/blob/7-14-stable/config/unicorn.rb.example but with your settings.
-* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.0/config.yml.example but with your settings.
+* Make `/home/git/gitlab-shell/config.yml` the same as https://gitlab.com/gitlab-org/gitlab-shell/blob/v2.6.5/config.yml.example but with your settings.
* Copy rack attack middleware config
```bash
diff --git a/doc/update/7.13-to-7.14.md b/doc/update/7.13-to-7.14.md
index 7c2d3f4498a..6dd9727fb49 100644
--- a/doc/update/7.13-to-7.14.md
+++ b/doc/update/7.13-to-7.14.md
@@ -63,7 +63,7 @@ sudo -u git -H git checkout 7-14-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch
-sudo -u git -H git checkout v2.6.4
+sudo -u git -H git checkout v2.6.5
```
### 5. Install libs, migrations, etc.
diff --git a/doc/update/7.14-to-8.0.md b/doc/update/7.14-to-8.0.md
index 29a38d07b3d..0e65c32098c 100644
--- a/doc/update/7.14-to-8.0.md
+++ b/doc/update/7.14-to-8.0.md
@@ -10,9 +10,9 @@ months after this vulnerability became known the GitLab installation guide
still contained instructions that would install the outdated, 'vulnerable' Git
version 2.1.2.
-Run the following command to get your current Git version.
+Run the following command to get your current Git version:
-```
+```sh
/usr/local/bin/git --version
```
@@ -63,39 +63,70 @@ sudo -u git -H git checkout 8-0-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch
-sudo -u git -H git checkout v2.6.4
+sudo -u git -H git checkout v2.6.5
```
### 5. Install gitlab-git-http-server
-First we download Go 1.5 and install it into /usr/local/go.
+First we download Go 1.5 and install it into `/usr/local/go`:
+
+```bash
+curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
+echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \
+ sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz
+sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
+rm go1.5.linux-amd64.tar.gz
+```
+
+Now we download `gitlab-git-http-server` and install it in `/home/git/gitlab-git-http-server`:
+
+```bash
+cd /home/git
+sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
+cd gitlab-git-http-server
+sudo -u git -H make
+```
+
+Make sure your unicorn.rb file contains a 'listen' line for
+'127.0.0.1:8080' and that this line is not commented out.
+
+```
+cd /home/git/gitlab
+grep ^listen config/unicorn.rb
- curl -O --progress https://storage.googleapis.com/golang/go1.5.linux-amd64.tar.gz
- echo '5817fa4b2252afdb02e11e8b9dc1d9173ef3bd5a go1.5.linux-amd64.tar.gz' | shasum -c - && \
- sudo tar -C /usr/local -xzf go1.5.linux-amd64.tar.gz
- sudo ln -sf /usr/local/go/bin/{go,godoc,gofmt} /usr/local/bin/
- rm go1.5.linux-amd64.tar.gz
+# If there is no 'listen' line for 127.0.0.1:8080, add it:
+sudo -u git tee -a config/unicorn.rb <<EOF
+listen "127.0.0.1:8080", :tcp_nopush => true
+EOF
+```
-Now we download gitlab-git-http-server and install it in /home/git/gitlab-git-http-server.
+If your Git repositories are in a directory other than `/home/git/repositories`,
+you need to tell `gitlab-git-http-server` about it via `/etc/default/gitlab`.
+See `lib/support/init.d/gitlab.default.example` for the options.
- cd /home/git
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-git-http-server.git
- cd gitlab-git-http-server
- sudo -u git -H make
+### 6. Copy secrets
-If you put your Git repositories in a directory different from /home/git/repositories, you need to tell gitlab-git-http-server about it via /etc/gitlab/default.
-See lib/support/init.d/gitlab.default.example for the options.
+The `secrets.yml` file is used to store keys to encrypt sessions and encrypt secure variables.
+When you run migrations make sure to store it someplace safe.
+Don't store it in the same place as your database backups,
+otherwise your secrets are exposed if one of your backups is compromised.
-### 6. Install libs, migrations, etc.
+```
+cd /home/git/gitlab
+sudo -u git -H cp config/secrets.yml.example config/secrets.yml
+sudo -u git -H chmod 0600 config/secrets.yml
+```
+
+### 7. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
-# MySQL installations (note: the line below states '--without ... postgres')
-sudo -u git -H bundle install --without development test postgres --deployment
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
-# PostgreSQL installations (note: the line below states '--without ... mysql')
-sudo -u git -H bundle install --without development test mysql --deployment
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
# Run database migrations
sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
@@ -107,38 +138,55 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
-### 7. Update config files
+### 8. Update config files
#### New configuration options for `gitlab.yml`
-There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them to your current `gitlab.yml`.
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
-```
+```sh
git diff origin/7-14-stable:config/gitlab.yml.example origin/8-0-stable:config/gitlab.yml.example
-``````
+```
-#### New NGINX configuration
+The new options include configuration of GitLab CI that are now being part of GitLab CE and EE.
-Because of the new gitlab-git-http-server you need to update your NGINX configuration.
-If you skip this step 'git clone' and 'git push' over HTTP(S) will stop working.
+#### New Nginx configuration
-```
-# Remove '-ssl' twice in the diff command below if you use HTTP instead of HTTPS
+Because of the new `gitlab-git-http-server` you need to update your Nginx
+configuration. If you skip this step 'git clone' and 'git push' over HTTP(S)
+will stop working.
+
+View changes between the previous recommended Nginx configuration and the
+current one:
+
+```sh
+# For HTTPS configurations
git diff origin/7-14-stable:lib/support/nginx/gitlab-ssl origin/8-0-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/7-14-stable:lib/support/nginx/gitlab origin/8-0-stable:lib/support/nginx/gitlab
```
-### 8. Start application
+If you are using Apache instead of NGINX please see the updated [Apache templates](https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache).
+Also note that because Apache does not support upstreams behind Unix sockets you will need to let gitlab-git-http-server listen on a TCP port. You can do this via [/etc/default/gitlab](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-0-stable/lib/support/init.d/gitlab.default.example#L34).
+
+### 9. Migrate GitLab CI to GitLab CE/EE
+
+Now, GitLab CE and EE has CI integrated. However, migrations don't happen automatically and you need to do it manually.
+Please follow the following guide [to migrate](../migrate_ci_to_ce/README.md) your GitLab CI instance to GitLab CE/EE.
+
+### 10. Start application
sudo service gitlab start
sudo service nginx restart
-### 9. Check application status
+### 11. Check application status
Check if GitLab and its environment are configured correctly:
sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
-To make sure you didn't miss anything run a more thorough check with:
+To make sure you didn't miss anything run a more thorough check:
sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
@@ -147,13 +195,25 @@ If all items are green, then congratulations, the upgrade is complete!
## Things went south? Revert to previous version (7.14)
### 1. Revert the code to the previous version
+
Follow the [upgrade guide from 7.13 to 7.14](7.13-to-7.14.md), except for the database migration
(The backup is already migrated to the previous version)
-### 2. Restore from the backup:
+### 2. Restore from the backup
```bash
cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
```
-If you have more than one backup *.tar file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
+
+## Troubleshooting
+
+### "You appear to have cloned an empty repository."
+
+If you see this message when attempting to clone a repository hosted by GitLab,
+this is likely due to an outdated Nginx or Apache configuration, or a missing or
+misconfigured `gitlab-git-http-server` instance. Double-check that you correctly
+completed [Step 5](#5-install-gitlab-git-http-server) to install the daemon and
+[Step 8](#new-nginx-configuration) to reconfigure Nginx.
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index 22b9be059d6..a66a863f6c4 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -1,7 +1,7 @@
# Universal update guide for patch versions
*Make sure you view this [upgrade guide](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/update/patch_versions.md) from the `master` branch for the most up to date instructions.*
-For example from 6.2.0 to 6.2.1, also see the [semantic versioning specification](http://semver.org/).
+For example from 7.14.0 to 7.14.3, also see the [semantic versioning specification](http://semver.org/).
### 0. Backup
@@ -23,17 +23,16 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
cd /home/git/gitlab
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- Gemfile.lock db/schema.rb
-sudo -u git -H git checkout LATEST_TAG
+LATEST_TAG=$(git describe --tags `git rev-list --tags --max-count=1`)
+sudo -u git -H git checkout $LATEST_TAG -b $LATEST_TAG
```
-Replace LATEST_TAG with the latest GitLab tag you want to upgrade to, for example `v6.6.3`.
-
### 3. Update gitlab-shell to the corresponding version
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch
-sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
+sudo -u git -H git checkout v`cat /home/git/gitlab/GITLAB_SHELL_VERSION` -b v`cat /home/git/gitlab/GITLAB_SHELL_VERSION`
```
### 4. Install libs, migrations, etc.
diff --git a/doc/update/upgrader.md b/doc/update/upgrader.md
index 6854250dab7..fd0327686b1 100644
--- a/doc/update/upgrader.md
+++ b/doc/update/upgrader.md
@@ -1,4 +1,4 @@
-# GitLab Upgrader
+# GitLab Upgrader (deprecated)
*DEPRECATED* We recommend to [switch to the Omnibus package and repository server](https://about.gitlab.com/update/) instead of using this script.
diff --git a/doc/web_hooks/ssl.png b/doc/web_hooks/ssl.png
new file mode 100644
index 00000000000..698f1a0f64a
--- /dev/null
+++ b/doc/web_hooks/ssl.png
Binary files differ
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 73717ffc7d6..c185ccfcac3 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -6,7 +6,15 @@ You can configure web hooks to listen for specific events like pushes, issues or
Web hooks can be used to update an external issue tracker, trigger CI builds, update a backup mirror, or even deploy to your production server.
-If you send a web hook to an SSL endpoint [the certificate will not be verified](https://gitlab.com/gitlab-org/gitlab-ce/blob/ccd617e58ea71c42b6b073e692447d0fe3c00be6/app/models/web_hook.rb#L35) since many people use self-signed certificates.
+## SSL Verification
+
+By default, the SSL certificate of the webhook endpoint is verified based on
+an internal list of Certificate Authorities,
+which means the certificate cannot be self-signed.
+
+You can turn this off in the web hook settings in your GitLab projects.
+
+![SSL Verification](ssl.png)
## Push events
@@ -34,7 +42,7 @@ X-Gitlab-Event: Push Hook
"name": "Diaspora",
"url": "git@example.com:mike/diasporadiaspora.git",
"description": "",
- "homepage": "http://example.com/mike/diaspora",
+ "homepage": "http://example.com/mike/diaspora",
"git_http_url":"http://example.com/mike/diaspora.git",
"git_ssh_url":"git@example.com:mike/diaspora.git",
"visibility_level":0
@@ -121,6 +129,12 @@ X-Gitlab-Event: Issue Hook
"username": "root",
"avatar_url": "http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=40\u0026d=identicon"
},
+ "repository": {
+ "name": "Gitlab Test",
+ "url": "http://example.com/gitlabhq/gitlab-test.git",
+ "description": "Aut reprehenderit ut est.",
+ "homepage": "http://example.com/gitlabhq/gitlab-test"
+ },
"object_attributes": {
"id": 301,
"title": "New API: create/update/delete file",
@@ -279,6 +293,7 @@ X-Gitlab-Event: Note Hook
"name": "Gitlab Test",
"ssh_url": "git@example.com:gitlab-org/gitlab-test.git",
"http_url": "http://example.com/gitlab-org/gitlab-test.git",
+ "web_url": "http://example.com/gitlab-org/gitlab-test",
"namespace": "Gitlab Org",
"visibility_level": 10
},
@@ -286,6 +301,7 @@ X-Gitlab-Event: Note Hook
"name": "Gitlab Test",
"ssh_url": "git@example.com:gitlab-org/gitlab-test.git",
"http_url": "http://example.com/gitlab-org/gitlab-test.git",
+ "web_url": "http://example.com/gitlab-org/gitlab-test",
"namespace": "Gitlab Org",
"visibility_level": 10
},
@@ -462,6 +478,7 @@ X-Gitlab-Event: Merge Request Hook
"name": "awesome_project",
"ssh_url": "ssh://git@example.com/awesome_space/awesome_project.git",
"http_url": "http://example.com/awesome_space/awesome_project.git",
+ "web_url": "http://example.com/awesome_space/awesome_project",
"visibility_level": 20,
"namespace": "awesome_space"
},
@@ -469,6 +486,7 @@ X-Gitlab-Event: Merge Request Hook
"name": "awesome_project",
"ssh_url": "ssh://git@example.com/awesome_space/awesome_project.git",
"http_url": "http://example.com/awesome_space/awesome_project.git",
+ "web_url": "http://example.com/awesome_space/awesome_project",
"visibility_level": 20,
"namespace": "awesome_space"
},
@@ -503,8 +521,8 @@ server.mount_proc '/' do |req, res|
puts req.body
end
-trap 'INT' do
- server.shutdown
+trap 'INT' do
+ server.shutdown
end
server.start
```
@@ -519,4 +537,4 @@ When you press 'Test Hook' in GitLab, you should see something like this in the
{"before":"077a85dd266e6f3573ef7e9ef8ce3343ad659c4e","after":"95cd4a99e93bc4bbabacfa2cd10e6725b1403c60",<SNIP>}
example.com - - [14/May/2014:07:45:26 EDT] "POST / HTTP/1.1" 200 0
- -> /
-```
+``` \ No newline at end of file
diff --git a/doc/workflow/importing/README.md b/doc/workflow/importing/README.md
index 5cde90993d2..7ccf06fbd60 100644
--- a/doc/workflow/importing/README.md
+++ b/doc/workflow/importing/README.md
@@ -3,6 +3,7 @@
1. [Bitbucket](import_projects_from_bitbucket.md)
2. [GitHub](import_projects_from_github.md)
3. [GitLab.com](import_projects_from_gitlab_com.md)
+4. [FogBugz](import_projects_from_fogbugz.md)
4. [SVN](migrating_from_svn.md)
### Note
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
new file mode 100644
index 00000000000..205c515bd3f
--- /dev/null
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_finished.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
new file mode 100644
index 00000000000..a1e348d46ad
--- /dev/null
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_login.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
new file mode 100644
index 00000000000..ed362846909
--- /dev/null
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_fogbogz.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
new file mode 100644
index 00000000000..d2fbd0267bd
--- /dev/null
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_select_project.png
Binary files differ
diff --git a/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
new file mode 100644
index 00000000000..b1cc4b58525
--- /dev/null
+++ b/doc/workflow/importing/fogbugz_importer/fogbugz_import_user_map.png
Binary files differ
diff --git a/doc/workflow/importing/import_projects_from_fogbugz.md b/doc/workflow/importing/import_projects_from_fogbugz.md
new file mode 100644
index 00000000000..71af0f9ea44
--- /dev/null
+++ b/doc/workflow/importing/import_projects_from_fogbugz.md
@@ -0,0 +1,29 @@
+# Import your project from FogBugz to GitLab
+
+It only takes a few simple steps to import your project from FogBugz.
+The importer will import all of your cases and comments with original case
+numbers and timestamps. You will also have the opportunity to map FogBugz
+users to GitLab users.
+
+* From your GitLab dashboard click 'New project'
+
+* Click on the 'FogBugz' button
+
+![FogBugz](fogbugz_importer/fogbugz_import_select_fogbogz.png)
+
+* Enter your FogBugz URL, email address, and password.
+
+![Login](fogbugz_importer/fogbugz_import_login.png)
+
+* Create mapping from FogBugz users to GitLab users.
+
+![User Map](fogbugz_importer/fogbugz_import_user_map.png)
+
+* Select the projects you wish to import by clicking the Import buttons
+
+![Import Project](fogbugz_importer/fogbugz_import_select_project.png)
+
+* Once the import has finished click the link to take you to the project
+dashboard. Follow the directions to push your existing repository.
+
+![Finished](fogbugz_importer/fogbugz_import_finished.png)
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 00000000000..e5a5d8dd53d
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,2 @@
+app:
+ image: gitlab/gitlab-ce:latest
diff --git a/docker/.dockerignore b/docker/.dockerignore
deleted file mode 100644
index dd449725e18..00000000000
--- a/docker/.dockerignore
+++ /dev/null
@@ -1 +0,0 @@
-*.md
diff --git a/docker/Dockerfile b/docker/Dockerfile
deleted file mode 100644
index 05521af6963..00000000000
--- a/docker/Dockerfile
+++ /dev/null
@@ -1,47 +0,0 @@
-FROM ubuntu:14.04
-MAINTAINER Sytse Sijbrandij
-
-# Install required packages
-RUN apt-get update -q \
- && DEBIAN_FRONTEND=noninteractive apt-get install -yq --no-install-recommends \
- ca-certificates \
- openssh-server \
- wget \
- apt-transport-https \
- vim \
- nano
-
-# Download & Install GitLab
-# If you run GitLab Enterprise Edition point it to a location where you have downloaded it.
-RUN echo "deb https://packages.gitlab.com/gitlab/gitlab-ce/ubuntu/ `lsb_release -cs` main" > /etc/apt/sources.list.d/gitlab_gitlab-ce.list
-RUN wget -q -O - https://packages.gitlab.com/gpg.key | apt-key add -
-RUN apt-get update && apt-get install -yq --no-install-recommends gitlab-ce
-
-# Manage SSHD through runit
-RUN mkdir -p /opt/gitlab/sv/sshd/supervise \
- && mkfifo /opt/gitlab/sv/sshd/supervise/ok \
- && printf "#!/bin/sh\nexec 2>&1\numask 077\nexec /usr/sbin/sshd -D" > /opt/gitlab/sv/sshd/run \
- && chmod a+x /opt/gitlab/sv/sshd/run \
- && ln -s /opt/gitlab/sv/sshd /opt/gitlab/service \
- && mkdir -p /var/run/sshd
-
-# Prepare default configuration
-RUN ( \
- echo "" && \
- echo "# Docker options" && \
- echo "# Prevent Postgres from trying to allocate 25% of total memory" && \
- echo "postgresql['shared_buffers'] = '1MB'" ) >> /etc/gitlab/gitlab.rb && \
- mkdir -p /assets/ && \
- cp /etc/gitlab/gitlab.rb /assets/gitlab.rb
-
-# Expose web & ssh
-EXPOSE 443 80 22
-
-# Define data volumes
-VOLUME ["/etc/gitlab", "/var/opt/gitlab", "/var/log/gitlab"]
-
-# Copy assets
-COPY assets/wrapper /usr/local/bin/
-
-# Wrapper to handle signal, trigger runit and reconfigure GitLab
-CMD ["/usr/local/bin/wrapper"]
diff --git a/docker/README.md b/docker/README.md
index e4d56cdb336..7514d610aec 100644
--- a/docker/README.md
+++ b/docker/README.md
@@ -1,169 +1,7 @@
# GitLab Docker images
-The GitLab docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/).
-
-## After starting a container
-
-After starting a container you can go to [http://localhost:8080/](http://localhost:8080/) or [http://192.168.59.103:8080/](http://192.168.59.103:8080/) if you use boot2docker.
-
-It might take a while before the docker container is responding to queries.
-
-You can check the status with something like `sudo docker logs -f gitlab`.
-
-You can login to the web interface with username `root` and password `5iveL!fe`.
-
-Next time, you can just use docker start and stop to run the container.
-
-## Run the image
-
-Run the image:
-```bash
-sudo docker run --detach \
- --publish 8443:443 --publish 8080:80 --publish 2222:22 \
- --name gitlab \
- --restart always \
- --volume /srv/gitlab/config:/etc/gitlab \
- --volume /srv/gitlab/logs:/var/log/gitlab \
- --volume /srv/gitlab/data:/var/opt/gitlab \
- gitlab/gitlab-ce:latest
-```
-
-This will download and start GitLab CE container and publish ports needed to access SSH, HTTP and HTTPS.
-All GitLab data will be stored as subdirectories of `/srv/gitlab/`.
-The container will automatically `restart` after system reboot.
-
-After this you can login to the web interface as explained above in 'After starting a container'.
-
-## Where is the data stored?
-
-The GitLab container uses host mounted volumes to store persistent data:
-- `/srv/gitlab/data` mounted as `/var/opt/gitlab` in the container is used for storing *application data*
-- `/srv/gitlab/logs` mounted as `/var/log/gitlab` in the container is used for storing *logs*
-- `/srv/gitlab/config` mounted as `/etc/gitlab` in the container is used for storing *configuration*
-
-You can fine tune these directories to meet your requirements.
-
-### Configure GitLab
-
-This container uses the official Omnibus GitLab distribution, so all configuration is done in the unique configuration file `/etc/gitlab/gitlab.rb`.
-
-To access GitLab configuration, you can start an bash in a new the context of running container, you will be able to browse all directories and use your favorite text editor:
-```bash
-sudo docker exec -it gitlab /bin/bash
-```
-
-You can also edit just `/etc/gitlab/gitlab.rb`:
-```bash
-sudo docker exec -it gitlab vi /etc/gitlab/gitlab.rb
-```
-
-**You should set the `external_url` to point to a valid URL.**
-
-**You may also be interesting in [Enabling HTTPS](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/nginx.md#enable-https).**
-
-**To receive e-mails from GitLab you have to configure the [SMTP settings](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/settings/smtp.md),
-because Docker image doesn't have a SMTP server.**
-
-**Note** that GitLab will reconfigure itself **at each container start.** You will need to restart the container to reconfigure your GitLab:
-
-```bash
-sudo docker restart gitlab
-```
-
-For more options for configuring the container please check [Omnibus GitLab documentation](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#configuration).
-
-## Diagnose potential problems
-
-Read container logs:
-```bash
-sudo docker logs gitlab
-```
-
-Enter running container:
-```bash
-sudo docker exec -it gitlab /bin/bash
-```
-
-From within container you can administrer GitLab container as you would normally administer Omnibus installation: https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md.
-
-### Upgrade GitLab to newer version
-
-To upgrade GitLab to new version you have to do:
-1. pull new image,
-```bash
-sudo docker stop gitlab
-```
-
-1. stop running container,
-```bash
-sudo docker rm gitlab
-```
-
-1. remove existing container,
-```bash
-sudo docker pull gitlab/gitlab-ce:latest
-```
-
-1. create the container once again with previously specified options.
-```bash
-sudo docker run --detach \
- --publish 8443:443 --publish 8080:80 --publish 2222:22 \
- --name gitlab \
- --restart always \
- --volume /srv/gitlab/config:/etc/gitlab \
- --volume /srv/gitlab/logs:/var/log/gitlab \
- --volume /srv/gitlab/data:/var/opt/gitlab \
- gitlab/gitlab-ce:latest
-```
-
-On the first run GitLab will reconfigure and update itself.
-
-### Run GitLab CE on public IP address
-
-You can make Docker to use your IP address and forward all traffic to the GitLab CE container.
-You can do that by modifying the `--publish` ([Binding container ports to the host](https://docs.docker.com/articles/networking/#binding-ports)):
-
-> --publish=[] : Publish a container᾿s port or a range of ports to the host format: ip:hostPort:containerPort | ip::containerPort | hostPort:containerPort | containerPort
-
-To expose GitLab CE on IP 1.1.1.1:
-
-```bash
-sudo docker run --detach \
- --publish 1.1.1.1:443:443 --publish 1.1.1.1:80:80 --publish 1.1.1.1:22:22 \
- --name gitlab \
- --restart always \
- --volume /srv/gitlab/config:/etc/gitlab \
- --volume /srv/gitlab/logs:/var/log/gitlab \
- --volume /srv/gitlab/data:/var/opt/gitlab \
- gitlab/gitlab-ce:latest
-```
-
-You can then access GitLab instance at http://1.1.1.1/ and https://1.1.1.1/.
-
-### Build the image
-
-This guide will also let you know how to build docker image yourself.
-Please run the command from the GitLab repo root directory.
-People using boot2docker should run all the commands without sudo.
-
-```bash
-sudo docker build --tag gitlab/gitlab-ce:latest docker/
-```
-
-### Publish the image to Dockerhub
-
-- Ensure the containers are running
-- Login to Dockerhub with `sudo docker login`
-
-```bash
-sudo docker login
-sudo docker push gitlab/gitlab-ce:latest
-```
-
-## Troubleshooting
-
-Please see the [troubleshooting](troubleshooting.md) file in this directory.
-
-Note: We use `fig.yml` to have compatibility with fig and because docker-compose also supports it.
-
-Our docker image runs chef at every start to generate GitLab configuration.
+* The official GitLab Community Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ce/).
+* The official GitLab Enterprise Edition Docker image is [available on Docker Hub](https://registry.hub.docker.com/u/gitlab/gitlab-ee/).
+* The complete usage guide can be found in [Using GitLab Docker images](http://doc.gitlab.com/omnibus/docker/)
+* The Dockerfile used for building public images is in [Omnibus Repository](https://gitlab.com/gitlab-org/omnibus-gitlab/tree/master/docker)
+* Check the guide for [creating Omnibus-based Docker Image](http://doc.gitlab.com/omnibus/build/README.html#Build-Docker-image)
diff --git a/docker/assets/wrapper b/docker/assets/wrapper
deleted file mode 100755
index 8bc8370fbc9..00000000000
--- a/docker/assets/wrapper
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/bin/bash
-
-function sigterm_handler() {
- echo "SIGTERM signal received, try to gracefully shutdown all services..."
- gitlab-ctl stop
-}
-
-trap "sigterm_handler; exit" TERM
-
-function entrypoint() {
- /opt/gitlab/embedded/bin/runsvdir-start &
- gitlab-ctl reconfigure # will also start everything
- gitlab-ctl tail # tail all logs
-}
-
-if [[ ! -e /etc/gitlab/gitlab.rb ]]; then
- cp /assets/gitlab.rb /etc/gitlab/gitlab.rb
- chmod 0600 /etc/gitlab/gitlab.rb
-fi
-
-entrypoint
diff --git a/docker/fig.yml b/docker/fig.yml
deleted file mode 100644
index 989551cbfe2..00000000000
--- a/docker/fig.yml
+++ /dev/null
@@ -1,2 +0,0 @@
-app:
- build: .
diff --git a/docker/marathon.json b/docker/marathon.json
deleted file mode 100644
index 9b2091a8c22..00000000000
--- a/docker/marathon.json
+++ /dev/null
@@ -1,31 +0,0 @@
-{
- "id": "/gitlab",
- "ports": [0,0],
- "cpus": 2,
- "mem": 2048.0,
- "disk": 10240.0,
- "container": {
- "type": "DOCKER",
- "docker": {
- "network": "HOST",
- "image": "gitlab/gitlab-ce:latest"
- },
- "volumes": [
- {
- "containerPath": "/etc/gitlab",
- "hostPath": "/var/data/etc/gitlab",
- "mode": "RW"
- },
- {
- "containerPath": "/var/opt/gitlab",
- "hostPath": "/var/data/opt/gitlab",
- "mode": "RW"
- },
- {
- "containerPath": "/var/log/gitlab",
- "hostPath": "/var/data/log/gitlab",
- "mode": "RW"
- }
- ]
- }
-} \ No newline at end of file
diff --git a/docker/troubleshooting.md b/docker/troubleshooting.md
deleted file mode 100644
index 63482547daa..00000000000
--- a/docker/troubleshooting.md
+++ /dev/null
@@ -1,84 +0,0 @@
-# Troubleshooting
-
-This is to troubleshoot https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/245
-But it might contain useful commands for other cases as well.
-
-The configuration to add the postgres log in vim is:
-postgresql['log_directory'] = '/var/log/gitlab/postgresql'
-
-# Commands
-
-```bash
-sudo docker build --tag gitlab/gitlab-ce:latest docker/
-
-sudo docker rm -f gitlab
-
-sudo docker exec -it gitlab vim /etc/gitlab/gitlab.rb
-
-sudo docker exec gitlab tail -f /var/log/gitlab/reconfigure.log
-
-sudo docker exec gitlab tail -f /var/log/gitlab/postgresql/current
-
-sudo docker exec gitlab cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers
-
-sudo docker exec gitlab cat /etc/gitlab/gitlab.rb
-```
-
-# Interactively
-
-```bash
-# First start a GitLab container without starting GitLab
-# This is almost the same as starting the GitLab container except:
-# - we run interactively (-t -i)
-# - we define TERM=linux because it allows to use arrow keys in vi (!!!)
-# - we choose another startup command (bash)
-sudo docker run --ti \
- -e TERM=linux
- --publish 80443:443 --publish 8080:80 --publish 2222:22 \
- --name gitlab \
- --restart always \
- --volume /srv/gitlab/config:/etc/gitlab \
- --volume /srv/gitlab/logs:/var/log/gitlab \
- --volume /srv/gitlab/data:/var/opt/gitlab \
- gitlab/gitlab-ce:latest \
- bash
-
-# Configure GitLab to redirect PostgreSQL logs
-echo "postgresql['log_directory'] = '/var/log/gitlab/postgresql'" >> /etc/gitlab/gitlab.rb
-
-# Prevent Postgres from allocating 25% of total memory
-echo "postgresql['shared_buffers'] = '1MB'" >> /etc/gitlab/gitlab.rb
-
-# You can now start GitLab manually from Bash (in the background)
-# Maybe the command below is still missing something to run in the background
-gitlab-ctl reconfigure > /var/log/gitlab/reconfigure.log & /opt/gitlab/embedded/bin/runsvdir-start &
-
-# Inspect PostgreSQL config
-cat /var/opt/gitlab/postgresql/data/postgresql.conf | grep shared_buffers
-
-# And tail the logs (PostgreSQL log may not exist immediately)
-tail -f /var/log/gitlab/reconfigure.log /var/log/gitlab/postgresql/current
-
-# And get the memory
-cat /proc/meminfo
-head /proc/sys/kernel/shmmax /proc/sys/kernel/shmall
-free -m
-
-```
-
-# Cleanup
-
-Remove ALL docker containers and images (also non GitLab ones).
-**Be careful, because the `-v` also removes volumes attached to the images.**
-
-```bash
-# Remove all containers with attached volumes
-docker rm -v $(docker ps -a -q)
-
-# Remove all images
-docker rmi $(docker images -q)
-
-# Remove GitLab persistent data
-rm -rf /srv/gitlab
-```
-
diff --git a/features/abuse_report.feature b/features/abuse_report.feature
index 3e1cb455b77..212972a762a 100644
--- a/features/abuse_report.feature
+++ b/features/abuse_report.feature
@@ -8,3 +8,10 @@ Feature: Abuse reports
And I click "Report abuse" button
When I fill and submit abuse form
Then I should see success message
+
+ Scenario: Report abuse available only once
+ Given I visit "Mike" user page
+ And I click "Report abuse" button
+ When I fill and submit abuse form
+ And I visit "Mike" user page
+ Then I should see a red "Report abuse" button
diff --git a/features/admin/labels.feature b/features/admin/labels.feature
new file mode 100644
index 00000000000..1af0e700bd4
--- /dev/null
+++ b/features/admin/labels.feature
@@ -0,0 +1,38 @@
+Feature: Admin Issues Labels
+ Background:
+ Given I sign in as an admin
+ And I have labels: "bug", "feature", "enhancement"
+ Given I visit admin labels page
+
+ Scenario: I should see labels list
+ Then I should see label 'bug'
+ And I should see label 'feature'
+
+ Scenario: I create new label
+ Given I submit new label 'support'
+ Then I should see label 'support'
+
+ Scenario: I edit label
+ Given I visit 'bug' label edit page
+ When I change label 'bug' to 'fix'
+ Then I should not see label 'bug'
+ Then I should see label 'fix'
+
+ Scenario: I remove label
+ When I remove label 'bug'
+ Then I should not see label 'bug'
+
+ @javascript
+ Scenario: I delete all labels
+ When I delete all labels
+ Then I should see labels help message
+
+ Scenario: I create a label with invalid color
+ Given I visit admin new label page
+ When I submit new label with invalid color
+ Then I should see label color error message
+
+ Scenario: I create a label that already exists
+ Given I visit admin new label page
+ When I submit new label 'bug'
+ Then I should see label exist error message
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index 392d4235eff..b667b587c5b 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -4,12 +4,14 @@ Feature: Dashboard
Given I sign in as a user
And I own project "Shop"
And project "Shop" has push event
+ And project "Shop" has CI enabled
+ And project "Shop" has CI build
And I visit dashboard page
- @javascript
Scenario: I should see projects list
Then I should see "New Project" link
Then I should see "Shop" project link
+ Then I should see "Shop" project CI status
@javascript
Scenario: I should see activity list
diff --git a/features/explore/groups.feature b/features/explore/groups.feature
index c11634bd74a..a42e59c98f2 100644
--- a/features/explore/groups.feature
+++ b/features/explore/groups.feature
@@ -3,20 +3,6 @@ Feature: Explore Groups
Background:
Given group "TestGroup" has private project "Enterprise"
- Scenario: I should not see group with private projects as visitor
- When I visit group "TestGroup" page
- Then I should be redirected to sign in page
-
- Scenario: I should not see group with private projects group as user
- When I sign in as a user
- And I visit group "TestGroup" page
- Then page status code should be 404
-
- Scenario: I should not see group with private and internal projects as visitor
- Given group "TestGroup" has internal project "Internal"
- When I visit group "TestGroup" page
- Then I should be redirected to sign in page
-
Scenario: I should see group with private and internal projects as user
Given group "TestGroup" has internal project "Internal"
When I sign in as a user
diff --git a/features/groups.feature b/features/groups.feature
index d5272fdddcf..db37fa3b375 100644
--- a/features/groups.feature
+++ b/features/groups.feature
@@ -159,3 +159,14 @@ Feature: Groups
When I visit group "Owned" projects page
Then I should see group "Owned" projects list
And I should see "archived" label
+
+ # Public group
+ @javascript
+ Scenario: Signed out user should see group
+ Given "Mary Jane" is owner of group "Owned"
+ And I am a signed out user
+ And Group "Owned" has a public project "Public-project"
+ When I visit group "Owned" page
+ Then I should see group "Owned"
+ Then I should see project "Public-project"
+
diff --git a/features/login_form.feature b/features/login_form.feature
new file mode 100644
index 00000000000..b4d95754482
--- /dev/null
+++ b/features/login_form.feature
@@ -0,0 +1,5 @@
+Feature: Login form
+ Scenario: I see crowd form
+ Given Crowd integration enabled
+ When I visit sign in page
+ Then I should see Crowd login form \ No newline at end of file
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 3ebc8a39aae..e4beeb59adc 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -16,6 +16,13 @@ Feature: Project Commits
Then I see commit info
And I see side-by-side diff button
+ Scenario: I browse commit with ci from list
+ Given commit has ci status
+ And I click on commit link
+ Then I see commit ci info
+ And I click status link
+ Then I see builds list
+
Scenario: I browse commit with side-by-side diff view
Given I click on commit link
And I click side-by-side diff button
diff --git a/features/project/graph.feature b/features/project/graph.feature
index 89064242c1c..2acd65aea5f 100644
--- a/features/project/graph.feature
+++ b/features/project/graph.feature
@@ -12,3 +12,9 @@ Feature: Project Graph
Scenario: I should see project commits graphs
When I visit project "Shop" commits graph page
Then page should have commits graphs
+
+ @javascript
+ Scenario: I should see project ci graphs
+ Given project "Shop" has CI enabled
+ When I visit project "Shop" CI graph page
+ Then page should have CI graphs
diff --git a/features/project/issues/milestones.feature b/features/project/issues/milestones.feature
index bfbaaec5a35..c1a20e9b488 100644
--- a/features/project/issues/milestones.feature
+++ b/features/project/issues/milestones.feature
@@ -12,13 +12,17 @@ Feature: Project Issues Milestones
Given I click link "v2.2"
Then I should see milestone "v2.2"
- Scenario: I create new milestone
+ @javascript
+ Scenario: I create and delete new milestone
Given I click link "New Milestone"
And I submit new milestone "v2.3"
Then I should see milestone "v2.3"
+ Given I click link to remove milestone
+ When I visit project "Shop" activity page
+ Then I should see deleted milestone activity
Scenario: I delete new milestone
- Given I click link to remove milestone "v2.2"
+ Given I click link to remove milestone
And I should see no milestones
@javascript
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 947f668e432..83055188bac 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -115,40 +115,40 @@ Feature: Project Merge Requests
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is wrong" on line 39 of the second file
- And I click link "Hide inline discussion" of the second file
- Then I should not see a comment like "Line is wrong here" in the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
+ And I click link "Hide inline discussion" of the third file
+ Then I should not see a comment like "Line is wrong here" in the third file
@javascript
Scenario: I show comments on a merge request diff with comments in a single file
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is wrong" on line 39 of the second file
- Then I should see a comment like "Line is wrong" in the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
+ Then I should see a comment like "Line is wrong" in the third file
@javascript
Scenario: I hide comments on a merge request diff with comments in multiple files
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the first file
- And I leave a comment like "Line is wrong" on line 39 of the second file
- And I click link "Hide inline discussion" of the second file
- Then I should not see a comment like "Line is wrong here" in the second file
- And I should still see a comment like "Line is correct" in the first file
+ And I leave a comment like "Line is correct" on line 12 of the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
+ And I click link "Hide inline discussion" of the third file
+ Then I should not see a comment like "Line is wrong here" in the third file
+ And I should still see a comment like "Line is correct" in the second file
@javascript
Scenario: I show comments on a merge request diff with comments in multiple files
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the first file
- And I leave a comment like "Line is wrong" on line 39 of the second file
- And I click link "Hide inline discussion" of the second file
- And I click link "Show inline discussion" of the second file
- Then I should see a comment like "Line is wrong" in the second file
- And I should still see a comment like "Line is correct" in the first file
+ And I leave a comment like "Line is correct" on line 12 of the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
+ And I click link "Hide inline discussion" of the third file
+ And I click link "Show inline discussion" of the third file
+ Then I should see a comment like "Line is wrong" in the third file
+ And I should still see a comment like "Line is correct" in the second file
@javascript
Scenario: I unfold diff
@@ -163,8 +163,8 @@ Feature: Project Merge Requests
Given project "Shop" have "Bug NS-05" open merge request with diffs inside
And I visit merge request page "Bug NS-05"
And I click on the Changes tab
- And I leave a comment like "Line is correct" on line 12 of the first file
- And I leave a comment like "Line is wrong" on line 39 of the second file
+ And I leave a comment like "Line is correct" on line 12 of the second file
+ And I leave a comment like "Line is wrong" on line 39 of the third file
And I click Side-by-side Diff tab
Then I should see comments on the side-by-side diff page
diff --git a/features/project/project.feature b/features/project/project.feature
index 089ffcba14a..b3fb0794547 100644
--- a/features/project/project.feature
+++ b/features/project/project.feature
@@ -74,3 +74,9 @@ Feature: Project
Given I disable snippets in project
When I visit project "Shop" page
Then I should not see "Snippets" button
+
+ @javascript
+ Scenario: I edit Project Notifications
+ Given I click notifications drop down button
+ When I choose Mention setting
+ Then I should see Notification saved message
diff --git a/features/project/service.feature b/features/project/service.feature
index fdff640ec85..5014b52b9f6 100644
--- a/features/project/service.feature
+++ b/features/project/service.feature
@@ -72,6 +72,7 @@ Feature: Project Services
And I click Atlassian Bamboo CI service link
And I fill Atlassian Bamboo CI settings
Then I should see Atlassian Bamboo CI service settings saved
+ And I should see empty field Change Password
Scenario: Activate jetBrains TeamCity CI service
When I visit project "Shop" services page
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index d3a77466a35..58574166ef3 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -35,6 +35,29 @@ Feature: Project Source Browse Files
And I should see its new content
@javascript
+ Scenario: I can upload file and commit
+ Given I click on "new file" link in repo
+ Then I can see new file page
+ And I can see "upload an existing one"
+ And I click on "upload"
+ And I upload a new text file
+ And I fill the upload file commit message
+ And I click on "Upload file"
+ Then I can see the new text file
+ And I can see the new commit message
+
+ @javascript
+ Scenario: I can replace file and commit
+ Given I click on ".gitignore" file in repo
+ And I see the ".gitignore"
+ And I click on "Replace"
+ And I replace it with a text file
+ And I fill the replace file commit message
+ And I click on "Replace file"
+ Then I can see the new text file
+ And I can see the replacement commit message
+
+ @javascript
Scenario: I can create and commit file and specify new branch
Given I click on "new file" link in repo
And I edit code
diff --git a/features/project/wiki.feature b/features/project/wiki.feature
index 2ebfa3c1660..af970ecf2d0 100644
--- a/features/project/wiki.feature
+++ b/features/project/wiki.feature
@@ -91,3 +91,15 @@ Feature: Project Wiki
And I view the page history of a Wiki page that has a path
Then I should see a non-escaped path
And I should see the page history
+
+ @javascript
+ Scenario: View an old page version of a Wiki page
+ Given I create a New page with paths
+ And I click on the "Pages" button
+ And I edit the Wiki page with a path
+ Then I should see a non-escaped path
+ And I should see the Editing page
+ And I change the content
+ Then I click on Page History
+ And I should see the page history
+ And I should see a link with a version ID
diff --git a/features/steps/abuse_reports.rb b/features/steps/abuse_reports.rb
index 8f9ddb2899f..56652ff6f05 100644
--- a/features/steps/abuse_reports.rb
+++ b/features/steps/abuse_reports.rb
@@ -22,6 +22,10 @@ class Spinach::Features::AbuseReports < Spinach::FeatureSteps
user_mike
end
+ step 'I should see a red "Report abuse" button' do
+ expect(find(:css, '.report_abuse')).to have_selector(:css, 'span.btn-close')
+ end
+
def user_mike
@user_mike ||= create(:user, name: 'Mike')
end
diff --git a/features/steps/admin/labels.rb b/features/steps/admin/labels.rb
new file mode 100644
index 00000000000..b45d98658bc
--- /dev/null
+++ b/features/steps/admin/labels.rb
@@ -0,0 +1,117 @@
+class Spinach::Features::AdminIssuesLabels < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedProject
+ include SharedPaths
+
+ step 'I visit \'bug\' label edit page' do
+ visit edit_admin_label_path(bug_label)
+ end
+
+ step 'I visit admin new label page' do
+ visit new_admin_label_path
+ end
+
+ step 'I visit admin labels page' do
+ visit admin_labels_path
+ end
+
+ step 'I remove label \'bug\'' do
+ page.within "#label_#{bug_label.id}" do
+ click_link 'Remove'
+ end
+ end
+
+ step 'I have labels: "bug", "feature", "enhancement"' do
+ ["bug", "feature", "enhancement"].each do |title|
+ Label.create(title: title, template: true)
+ end
+ end
+
+ step 'I delete all labels' do
+ page.within '.labels' do
+ page.all('.btn-remove').each do |remove|
+ remove.click
+ sleep 0.05
+ end
+ end
+ end
+
+ step 'I should see labels help message' do
+ page.within '.labels' do
+ expect(page).to have_content 'There are no labels yet'
+ end
+ end
+
+ step 'I submit new label \'support\'' do
+ visit new_admin_label_path
+ fill_in 'Title', with: 'support'
+ fill_in 'Background Color', with: '#F95610'
+ click_button 'Save'
+ end
+
+ step 'I submit new label \'bug\'' do
+ visit new_admin_label_path
+ fill_in 'Title', with: 'bug'
+ fill_in 'Background Color', with: '#F95610'
+ click_button 'Save'
+ end
+
+ step 'I submit new label with invalid color' do
+ visit new_admin_label_path
+ fill_in 'Title', with: 'support'
+ fill_in 'Background Color', with: '#12'
+ click_button 'Save'
+ end
+
+ step 'I should see label exist error message' do
+ page.within '.label-form' do
+ expect(page).to have_content 'Title has already been taken'
+ end
+ end
+
+ step 'I should see label color error message' do
+ page.within '.label-form' do
+ expect(page).to have_content 'Color is invalid'
+ end
+ end
+
+ step 'I should see label \'feature\'' do
+ page.within '.manage-labels-list' do
+ expect(page).to have_content 'feature'
+ end
+ end
+
+ step 'I should see label \'bug\'' do
+ page.within '.manage-labels-list' do
+ expect(page).to have_content 'bug'
+ end
+ end
+
+ step 'I should not see label \'bug\'' do
+ page.within '.manage-labels-list' do
+ expect(page).not_to have_content 'bug'
+ end
+ end
+
+ step 'I should see label \'support\'' do
+ page.within '.manage-labels-list' do
+ expect(page).to have_content 'support'
+ end
+ end
+
+ step 'I change label \'bug\' to \'fix\'' do
+ fill_in 'Title', with: 'fix'
+ fill_in 'Background Color', with: '#F15610'
+ click_button 'Save'
+ end
+
+ step 'I should see label \'fix\'' do
+ page.within '.manage-labels-list' do
+ expect(page).to have_content 'fix'
+ end
+ end
+
+ def bug_label
+ Label.templates.find_or_create_by(title: 'bug')
+ end
+end
diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb
index 7a6aec23af8..6acbf46eb20 100644
--- a/features/steps/admin/settings.rb
+++ b/features/steps/admin/settings.rb
@@ -7,6 +7,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
step 'I modify settings and save form' do
uncheck 'Gravatar enabled'
fill_in 'Home page URL', with: 'https://about.gitlab.com/'
+ fill_in 'Help page text', with: 'Example text'
click_button 'Save'
end
diff --git a/features/steps/admin/users.rb b/features/steps/admin/users.rb
index 2e17d5c4c2e..4bc290b6bdf 100644
--- a/features/steps/admin/users.rb
+++ b/features/steps/admin/users.rb
@@ -4,11 +4,13 @@ class Spinach::Features::AdminUsers < Spinach::FeatureSteps
include SharedAdmin
before do
- allow(Devise).to receive(:omniauth_providers).and_return([:twitter, :twitter_updated])
+ allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
+ allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path)
end
after do
- allow(Devise).to receive(:omniauth_providers).and_call_original
+ allow(Gitlab::OAuth::Provider).to receive(:providers).and_call_original
+ allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_call_original
end
step 'I should see all users' do
diff --git a/features/steps/dashboard/dashboard.rb b/features/steps/dashboard/dashboard.rb
index cb3a80cac29..f0fbd8a826a 100644
--- a/features/steps/dashboard/dashboard.rb
+++ b/features/steps/dashboard/dashboard.rb
@@ -11,6 +11,10 @@ class Spinach::Features::Dashboard < Spinach::FeatureSteps
expect(page).to have_link "Shop"
end
+ step 'I should see "Shop" project CI status' do
+ expect(page).to have_link "Build status: skipped"
+ end
+
step 'I should see last push widget' do
expect(page).to have_content "You pushed to fix"
expect(page).to have_link "Create Merge Request"
diff --git a/features/steps/dashboard/starred_projects.rb b/features/steps/dashboard/starred_projects.rb
index 59c73fe63f2..c33813e550b 100644
--- a/features/steps/dashboard/starred_projects.rb
+++ b/features/steps/dashboard/starred_projects.rb
@@ -8,7 +8,7 @@ class Spinach::Features::DashboardStarredProjects < Spinach::FeatureSteps
end
step 'I should not see project "Shop"' do
- page.within 'aside' do
+ page.within '.projects-list' do
expect(page).not_to have_content('Shop')
end
end
diff --git a/features/steps/groups.rb b/features/steps/groups.rb
index 18a1c4d32ce..69ddfa42c06 100644
--- a/features/steps/groups.rb
+++ b/features/steps/groups.rb
@@ -6,7 +6,7 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
include Select2Helper
step 'I should see back to dashboard button' do
- expect(page).to have_content 'Back to Dashboard'
+ expect(page).to have_content 'Back to dashboard'
end
step 'gitlab user "Mike"' do
@@ -17,6 +17,26 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
find(:css, 'button.btn-new').click
end
+ step 'I should see group "Owned"' do
+ expect(page).to have_content '@owned'
+ end
+
+ step 'I am a signed out user' do
+ logout
+ end
+
+ step 'Group "Owned" has a public project "Public-project"' do
+ group = Group.find_by(name: "Owned")
+
+ @project = create :empty_project, :public,
+ group: group,
+ name: "Public-project"
+ end
+
+ step 'I should see project "Public-project"' do
+ expect(page).to have_content 'Public-project'
+ end
+
step 'I select "Mike" as "Reporter"' do
user = User.find_by(name: "Mike")
@@ -262,9 +282,9 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
milestone1_project2 = create :milestone,
title: "Version 7.2",
project: project2
- milestone1_project3 = create :milestone,
- title: "Version 7.2",
- project: @project3
+ create :milestone,
+ title: "Version 7.2",
+ project: @project3
milestone2_project1 = create :milestone,
title: "GL-113",
project: @project1
@@ -281,28 +301,28 @@ class Spinach::Features::Groups < Spinach::FeatureSteps
assignee: current_user,
author: current_user,
milestone: milestone2_project1
- issue2 = create :issue,
- project: project2,
- assignee: current_user,
- author: current_user,
- milestone: milestone1_project2
- issue3 = create :issue,
- project: @project3,
- assignee: current_user,
- author: current_user,
- milestone: milestone1_project1
- mr1 = create :merge_request,
- source_project: @project1,
- target_project: @project1,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project1
- mr2 = create :merge_request,
- source_project: project2,
- target_project: project2,
- assignee: current_user,
- author: current_user,
- milestone: milestone2_project2
+ create :issue,
+ project: project2,
+ assignee: current_user,
+ author: current_user,
+ milestone: milestone1_project2
+ create :issue,
+ project: @project3,
+ assignee: current_user,
+ author: current_user,
+ milestone: milestone1_project1
+ create :merge_request,
+ source_project: @project1,
+ target_project: @project1,
+ assignee: current_user,
+ author: current_user,
+ milestone: milestone2_project1
+ create :merge_request,
+ source_project: project2,
+ target_project: project2,
+ assignee: current_user,
+ author: current_user,
+ milestone: milestone2_project2
@mr3 = create :merge_request,
source_project: @project3,
target_project: @project3,
diff --git a/features/steps/invites.rb b/features/steps/invites.rb
index 5e8feff5095..dac972172aa 100644
--- a/features/steps/invites.rb
+++ b/features/steps/invites.rb
@@ -63,7 +63,7 @@ class Spinach::Features::Invites < Spinach::FeatureSteps
end
step 'I should be redirected to the dashboard' do
- expect(current_path).to eq(dashboard_path)
+ expect(current_path).to eq(dashboard_projects_path)
end
step 'I should see a notice telling me I have declined' do
diff --git a/features/steps/login_form.rb b/features/steps/login_form.rb
new file mode 100644
index 00000000000..b9ff6ae67fd
--- /dev/null
+++ b/features/steps/login_form.rb
@@ -0,0 +1,25 @@
+class Spinach::Features::LoginForm < Spinach::FeatureSteps
+ include SharedAuthentication
+ include SharedPaths
+ include SharedSnippet
+ include SharedUser
+ include SharedSearch
+
+ step 'Crowd integration enabled' do
+ @providers_orig = Gitlab::OAuth::Provider.providers
+ @omniauth_conf_orig = Gitlab.config.omniauth.enabled
+ expect(Gitlab::OAuth::Provider).to receive(:providers).and_return([:crowd])
+ allow_any_instance_of(ApplicationHelper).to receive(:user_omniauth_authorize_path).and_return(root_path)
+ expect(Gitlab.config.omniauth).to receive(:enabled).and_return(true)
+ end
+
+ step 'I should see Crowd login form' do
+ expect(page).to have_selector '#tab-crowd form'
+ Gitlab::OAuth::Provider.stub(:providers).and_return(@providers_orig)
+ Gitlab.config.omniauth.stub(:enabled).and_return(@omniauth_conf_orig)
+ end
+
+ step 'I visit sign in page' do
+ visit new_user_session_path
+ end
+end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index a8532cc18d8..ae5f90004e6 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -52,7 +52,6 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see compared refs' do
- expect(page).to have_content "Compare View"
expect(page).to have_content "Commits (1)"
expect(page).to have_content "Showing 2 changed files"
end
@@ -98,4 +97,27 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
step 'I see inline diff button' do
expect(page).to have_content "Inline"
end
+
+ step 'I click side-by-side diff button' do
+ find('#parallel-diff-btn').click
+ end
+
+ step 'commit has ci status' do
+ @project.enable_ci
+ ci_commit = create :ci_commit, gl_project: @project, sha: sample_commit.id
+ create :ci_build, commit: ci_commit
+ end
+
+ step 'I see commit ci info' do
+ expect(page).to have_content "build: pending"
+ end
+
+ step 'I click status link' do
+ click_link "Builds"
+ end
+
+ step 'I see builds list' do
+ expect(page).to have_content "build: pending"
+ expect(page).to have_content "Builds for master"
+ end
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 0e433781d7a..b0230add34f 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -5,8 +5,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'I click link "Fork"' do
expect(page).to have_content "Shop"
- expect(page).to have_content "Fork"
- click_link "Fork"
+ click_link "Fork project"
end
step 'I am a member of project "Shop"' do
@@ -15,7 +14,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I should see the forked project page' do
- expect(page).to have_content "Project was successfully forked."
+ expect(page).to have_content "Forked from"
end
step 'I already have a project named "Shop" in my namespace' do
diff --git a/features/steps/project/graph.rb b/features/steps/project/graph.rb
index 5e7e573a6ab..4abd5288d51 100644
--- a/features/steps/project/graph.rb
+++ b/features/steps/project/graph.rb
@@ -7,12 +7,10 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
end
When 'I visit project "Shop" graph page' do
- project = Project.find_by(name: "Shop")
visit namespace_project_graph_path(project.namespace, project, "master")
end
step 'I visit project "Shop" commits graph page' do
- project = Project.find_by(name: "Shop")
visit commits_namespace_project_graph_path(project.namespace, project, "master")
end
@@ -20,4 +18,20 @@ class Spinach::Features::ProjectGraph < Spinach::FeatureSteps
expect(page).to have_content "Commit statistics for master"
expect(page).to have_content "Commits per day of month"
end
+
+ step 'I visit project "Shop" CI graph page' do
+ visit ci_namespace_project_graph_path(project.namespace, project, 'master')
+ end
+
+ step 'page should have CI graphs' do
+ expect(page).to have_content 'Overall'
+ expect(page).to have_content 'Builds chart for last week'
+ expect(page).to have_content 'Builds chart for last month'
+ expect(page).to have_content 'Builds chart for last year'
+ expect(page).to have_content 'Commit duration in minutes for last 30 commits'
+ end
+
+ def project
+ @project ||= Project.find_by(name: "Shop")
+ end
end
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index 239392eab96..af2da41badb 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -223,11 +223,11 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
end
step 'project \'Shop\' has issue \'Bugfix1\' with description: \'Description for issue1\'' do
- issue = create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project)
+ create(:issue, title: 'Bugfix1', description: 'Description for issue1', project: project)
end
step 'project \'Shop\' has issue \'Feature1\' with description: \'Feature submitted for issue1\'' do
- issue = create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project)
+ create(:issue, title: 'Feature1', description: 'Feature submitted for issue1', project: project)
end
step 'I fill in issue search with \'Description for issue1\'' do
diff --git a/features/steps/project/issues/milestones.rb b/features/steps/project/issues/milestones.rb
index 61e62c2adbd..c8708572ec6 100644
--- a/features/steps/project/issues/milestones.rb
+++ b/features/steps/project/issues/milestones.rb
@@ -49,6 +49,11 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
create(:closed_issue, project: project, milestone: milestone)
end
+ step 'I should see deleted milestone activity' do
+ expect(page).to have_content('opened milestone in')
+ expect(page).to have_content('destroyed milestone in')
+ end
+
When 'I click link "All Issues"' do
click_link 'All Issues'
end
@@ -57,7 +62,7 @@ class Spinach::Features::ProjectIssuesMilestones < Spinach::FeatureSteps
expect(page).to have_selector('#tab-issues li.issue-row', count: 4)
end
- step 'I click link to remove milestone "v2.2"' do
+ step 'I click link to remove milestone' do
click_link 'Remove'
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 778dce06359..875bf6c4676 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -128,7 +128,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see the proper Inline and Side-by-side links' do
- expect(page).to have_css('#commit-diff-viewtype', count: 2)
+ expect(page).to have_css('#parallel-diff-btn', count: 1)
+ expect(page).to have_css('#inline-diff-btn', count: 1)
end
step 'I switch to the merge request\'s comments tab' do
@@ -223,43 +224,43 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
end
- step 'I click link "Hide inline discussion" of the second file' do
- page.within '.files [id^=diff]:nth-child(2)' do
- find('.js-toggle-diff-comments').click
+ step 'I click link "Hide inline discussion" of the third file' do
+ page.within '.files [id^=diff]:nth-child(3)' do
+ find('.js-toggle-diff-comments').trigger('click')
end
end
- step 'I click link "Show inline discussion" of the second file' do
- page.within '.files [id^=diff]:nth-child(2)' do
- find('.js-toggle-diff-comments').click
+ step 'I click link "Show inline discussion" of the third file' do
+ page.within '.files [id^=diff]:nth-child(3)' do
+ find('.js-toggle-diff-comments').trigger('click')
end
end
- step 'I should not see a comment like "Line is wrong" in the second file' do
- page.within '.files [id^=diff]:nth-child(2)' do
+ step 'I should not see a comment like "Line is wrong" in the third file' do
+ page.within '.files [id^=diff]:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong"
end
end
- step 'I should see a comment like "Line is wrong" in the second file' do
- page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do
+ step 'I should see a comment like "Line is wrong" in the third file' do
+ page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong"
end
end
- step 'I should not see a comment like "Line is wrong here" in the second file' do
- page.within '.files [id^=diff]:nth-child(2)' do
+ step 'I should not see a comment like "Line is wrong here" in the third file' do
+ page.within '.files [id^=diff]:nth-child(3)' do
expect(page).not_to have_visible_content "Line is wrong here"
end
end
- step 'I should see a comment like "Line is wrong here" in the second file' do
- page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do
+ step 'I should see a comment like "Line is wrong here" in the third file' do
+ page.within '.files [id^=diff]:nth-child(3) .note-body > .note-text' do
expect(page).to have_visible_content "Line is wrong here"
end
end
- step 'I leave a comment like "Line is correct" on line 12 of the first file' do
+ step 'I leave a comment like "Line is correct" on line 12 of the second file' do
init_diff_note_first_file
page.within(".js-discussion-note-form") do
@@ -267,12 +268,12 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
click_button "Add Comment"
end
- page.within ".files [id^=diff]:nth-child(1) .note-body > .note-text" do
+ page.within ".files [id^=diff]:nth-child(2) .note-body > .note-text" do
expect(page).to have_content "Line is correct"
end
end
- step 'I leave a comment like "Line is wrong" on line 39 of the second file' do
+ step 'I leave a comment like "Line is wrong" on line 39 of the third file' do
init_diff_note_second_file
page.within(".js-discussion-note-form") do
@@ -281,8 +282,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
end
- step 'I should still see a comment like "Line is correct" in the first file' do
- page.within '.files [id^=diff]:nth-child(1) .note-body > .note-text' do
+ step 'I should still see a comment like "Line is correct" in the second file' do
+ page.within '.files [id^=diff]:nth-child(2) .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
@@ -302,7 +303,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I should see comments on the side-by-side diff page' do
- page.within '.files [id^=diff]:nth-child(1) .parallel .note-body > .note-text' do
+ page.within '.files [id^=diff]:nth-child(2) .parallel .note-body > .note-text' do
expect(page).to have_visible_content "Line is correct"
end
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 0404fd5e594..15f77734cb2 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -124,10 +124,24 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see back to dashboard button' do
- expect(page).to have_content 'Back to Dashboard'
+ expect(page).to have_content 'Back to dashboard'
end
step 'I should see back to group button' do
- expect(page).to have_content 'Back to Group'
+ expect(page).to have_content 'Back to group'
+ end
+
+ step 'I click notifications drop down button' do
+ click_link 'notifications-button'
+ end
+
+ step 'I choose Mention setting' do
+ click_link 'On mention'
+ end
+
+ step 'I should see Notification saved message' do
+ page.within '.flash-container' do
+ expect(page).to have_content 'Notification settings saved'
+ end
end
end
diff --git a/features/steps/project/redirects.rb b/features/steps/project/redirects.rb
index 0e724138a8a..1ffd5cb9de5 100644
--- a/features/steps/project/redirects.rb
+++ b/features/steps/project/redirects.rb
@@ -39,7 +39,6 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
step 'Authenticate' do
admin = create(:admin)
- project = Project.find_by(name: 'Community')
fill_in "user_login", with: admin.email
fill_in "user_password", with: admin.password
click_button "Sign in"
@@ -54,7 +53,6 @@ class Spinach::Features::ProjectRedirects < Spinach::FeatureSteps
step 'I get redirected to signin page where I sign in' do
admin = create(:admin)
- project = Project.find_by(name: 'Enterprise')
fill_in "user_login", with: admin.email
fill_in "user_password", with: admin.password
click_button "Sign in"
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 0327fd61981..1c700df0c63 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -26,13 +26,11 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
step 'I fill gitlab-ci settings' do
check 'Active'
- fill_in 'Project url', with: 'http://ci.gitlab.org/projects/3'
- fill_in 'Token', with: 'verySecret'
click_button 'Save'
end
step 'I should see service settings saved' do
- expect(find_field('Project url').value).to eq 'http://ci.gitlab.org/projects/3'
+ expect(find_field('Active').value).to eq '1'
end
step 'I click hipchat service link' do
@@ -204,6 +202,10 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
expect(find_field('Username').value).to eq 'user'
end
+ step 'I should see empty field Change Password' do
+ expect(find_field('Change Password').value).to be_nil
+ end
+
step 'I click JetBrains TeamCity CI service link' do
click_link 'JetBrains TeamCity CI'
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 5cb085db207..a1a49dd58a6 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
include SharedAuthentication
include SharedProject
@@ -78,7 +79,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I fill the commit message' do
- fill_in :commit_message, with: 'Not yet a commit message.'
+ fill_in :commit_message, with: 'Not yet a commit message.', visible: true
end
step 'I click link "Diff"' do
@@ -97,6 +98,14 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_button 'Remove file'
end
+ step 'I click on "Replace"' do
+ click_button "Replace"
+ end
+
+ step 'I click on "Replace file"' do
+ click_button 'Replace file'
+ end
+
step 'I see diff' do
expect(page).to have_css '.line_holder.new'
end
@@ -106,10 +115,55 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I can see new file page' do
- expect(page).to have_content "New file"
+ expect(page).to have_content "new file"
expect(page).to have_content "Commit message"
end
+ step 'I can see "upload an existing one"' do
+ expect(page).to have_content "upload an existing one"
+ end
+
+ step 'I click on "upload"' do
+ click_link 'upload'
+ end
+
+ step 'I click on "Upload file"' do
+ click_button 'Upload file'
+ end
+
+ step 'I can see the new commit message' do
+ expect(page).to have_content "New upload commit message"
+ end
+
+ step 'I upload a new text file' do
+ drop_in_dropzone test_text_file
+ end
+
+ step 'I fill the upload file commit message' do
+ page.within('#modal-upload-blob') do
+ fill_in :commit_message, with: 'New upload commit message'
+ end
+ end
+
+ step 'I replace it with a text file' do
+ drop_in_dropzone test_text_file
+ end
+
+ step 'I fill the replace file commit message' do
+ page.within('#modal-upload-blob') do
+ fill_in :commit_message, with: 'Replacement file commit message'
+ end
+ end
+
+ step 'I can see the replacement commit message' do
+ expect(page).to have_content "Replacement file commit message"
+ end
+
+ step 'I can see the new text file' do
+ expect(page).to have_content "Lorem ipsum dolor sit amet"
+ expect(page).to have_content "Sed ut perspiciatis unde omnis"
+ end
+
step 'I click on files directory' do
click_link 'files'
end
@@ -232,4 +286,29 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
def new_file_name
'not_a_file.md'
end
+
+ def drop_in_dropzone(file_path)
+ # Generate a fake input selector
+ page.execute_script <<-JS
+ var fakeFileInput = window.$('<input/>').attr(
+ {id: 'fakeFileInput', type: 'file'}
+ ).appendTo('body');
+ JS
+ # Attach the file to the fake input selector with Capybara
+ attach_file("fakeFileInput", file_path)
+ # Add the file to a fileList array and trigger the fake drop event
+ page.execute_script <<-JS
+ var fileList = [$('#fakeFileInput')[0].files[0]];
+ var e = jQuery.Event('drop', { dataTransfer : { files : fileList } });
+ $('.dropzone')[0].dropzone.listeners[0].events.drop(e);
+ JS
+ end
+
+ def test_text_file
+ File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt')
+ end
+
+ def test_image_file
+ File.join(Rails.root, 'spec', 'fixtures', 'banana_sample.gif')
+ end
end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index eebfaee1ede..02207dbffa6 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -3,7 +3,6 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
include SharedProject
include SharedNote
include SharedPaths
- include WikiHelper
step 'I click on the Cancel button' do
page.within(:css, ".form-actions") do
@@ -165,6 +164,10 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
click_on 'Page History'
end
+ step 'I click on Page History' do
+ click_on 'Page History'
+ end
+
step 'I should see the page history' do
expect(page).to have_content('History for')
end
@@ -174,6 +177,10 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
click_button "Search"
end
+ step 'I should see a link with a version ID' do
+ find('a[href*="?version_id"]')
+ end
+
def wiki
@project_wiki = ProjectWiki.new(project, current_user)
end
diff --git a/features/steps/shared/active_tab.rb b/features/steps/shared/active_tab.rb
index 92e099315d8..eb2ccd9d01e 100644
--- a/features/steps/shared/active_tab.rb
+++ b/features/steps/shared/active_tab.rb
@@ -6,7 +6,7 @@ module SharedActiveTab
end
def ensure_active_sub_tab(content)
- expect(find('div.content ul.nav-tabs li.active')).to have_content(content)
+ expect(find('div.content ul.center-top-menu li.active')).to have_content(content)
end
def ensure_active_sub_nav(content)
@@ -18,7 +18,7 @@ module SharedActiveTab
end
step 'no other sub tabs should be active' do
- expect(page).to have_selector('div.content ul.nav-tabs li.active', count: 1)
+ expect(page).to have_selector('div.content ul.center-top-menu li.active', count: 1)
end
step 'no other sub navs should be active' do
diff --git a/features/steps/shared/diff_note.rb b/features/steps/shared/diff_note.rb
index 27a95aeb19a..72621911a37 100644
--- a/features/steps/shared/diff_note.rb
+++ b/features/steps/shared/diff_note.rb
@@ -198,7 +198,7 @@ module SharedDiffNote
end
step 'I click side-by-side diff button' do
- click_link "Side-by-side"
+ find('#parallel-diff-btn').trigger('click')
end
step 'I see side-by-side diff button' do
diff --git a/features/steps/shared/group.rb b/features/steps/shared/group.rb
index 2d17fb34ccb..83a04576973 100644
--- a/features/steps/shared/group.rb
+++ b/features/steps/shared/group.rb
@@ -37,7 +37,7 @@ module SharedGroup
group = Group.find_by(name: groupname) || create(:group, name: groupname)
group.add_user(user, role)
project ||= create(:project, namespace: group, path: "project#{@project_count}")
- event ||= create(:closed_issue_event, project: project)
+ create(:closed_issue_event, project: project)
project.team << [user, :master]
@project_count += 1
end
diff --git a/features/steps/shared/paths.rb b/features/steps/shared/paths.rb
index b4deccb6520..eb978620da6 100644
--- a/features/steps/shared/paths.rb
+++ b/features/steps/shared/paths.rb
@@ -68,7 +68,7 @@ module SharedPaths
# ----------------------------------------
step 'I visit dashboard page' do
- visit dashboard_path
+ visit dashboard_projects_path
end
step 'I visit dashboard activity page' do
@@ -460,7 +460,7 @@ module SharedPaths
end
step 'I visit snippets page' do
- visit snippets_path
+ visit explore_snippets_path
end
step 'I visit new snippet page' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index ccbe8f96a4c..5744e455ebd 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -51,6 +51,11 @@ module SharedProject
visit namespace_project_path(project.namespace, project)
end
+ step 'I visit project "Shop" activity page' do
+ project = Project.find_by(name: 'Shop')
+ visit namespace_project_path(project.namespace, project)
+ end
+
step 'project "Shop" has push event' do
@project = Project.find_by(name: "Shop")
@@ -191,4 +196,14 @@ module SharedProject
create(:label, project: project, title: 'feature')
create(:label, project: project, title: 'enhancement')
end
+
+ step 'project "Shop" has CI enabled' do
+ project = Project.find_by(name: "Shop")
+ project.enable_ci
+ end
+
+ step 'project "Shop" has CI build' do
+ project = Project.find_by(name: "Shop")
+ create :ci_commit, gl_project: project, sha: project.commit.sha
+ end
end
diff --git a/features/steps/snippets/user.rb b/features/steps/snippets/user.rb
index 007fcb2893f..dea3256229f 100644
--- a/features/steps/snippets/user.rb
+++ b/features/steps/snippets/user.rb
@@ -4,7 +4,7 @@ class Spinach::Features::SnippetsUser < Spinach::FeatureSteps
include SharedSnippet
step 'I visit my snippets page' do
- visit user_snippets_path(current_user)
+ visit dashboard_snippets_path
end
step 'I should see "Personal snippet one" in snippets' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index eebd44ea5b6..c09488d3547 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -50,5 +50,6 @@ module API
mount Branches
mount Labels
mount Settings
+ mount Keys
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 1f9dd6bc152..9620d36ac41 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -8,7 +8,7 @@ module API
expose :id, :state, :avatar_url
expose :web_url do |user, options|
- Rails.application.routes.url_helpers.user_url(user)
+ Gitlab::Application.routes.url_helpers.user_url(user)
end
end
@@ -45,7 +45,7 @@ module API
class ProjectHook < Hook
expose :project_id, :push_events
- expose :issues_events, :merge_requests_events, :tag_push_events
+ expose :issues_events, :merge_requests_events, :tag_push_events, :note_events, :enable_ssl_verification
end
class ForkedFromProject < Grape::Entity
@@ -81,7 +81,7 @@ module API
expose :avatar_url
expose :web_url do |group, options|
- Rails.application.routes.url_helpers.group_url(group)
+ Gitlab::Application.routes.url_helpers.group_url(group)
end
end
@@ -199,6 +199,10 @@ module API
expose :id, :title, :key, :created_at
end
+ class SSHKeyWithUser < SSHKey
+ expose :user, using: Entities::UserFull
+ end
+
class Note < Grape::Entity
expose :id
expose :note, as: :body
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 1ebf9a1f022..549b1f9e9a7 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -55,6 +55,32 @@ module API
end
end
+ def project_service
+ @project_service ||= begin
+ underscored_service = params[:service_slug].underscore
+
+ if Service.available_services_names.include?(underscored_service)
+ user_project.build_missing_services
+
+ service_method = "#{underscored_service}_service"
+
+ send_service(service_method)
+ end
+ end
+
+ @project_service || not_found!("Service")
+ end
+
+ def send_service(service_method)
+ user_project.send(service_method)
+ end
+
+ def service_attributes
+ @service_attributes ||= project_service.fields.inject([]) do |arr, hash|
+ arr << hash[:name].to_sym
+ end
+ end
+
def find_group(id)
begin
group = Group.find(id)
@@ -122,15 +148,13 @@ module API
end
end
- def attributes_for_keys(keys)
+ def attributes_for_keys(keys, custom_params = nil)
attrs = {}
-
keys.each do |key|
if params[key].present? or (params.has_key?(key) and params[key] == false)
attrs[key] = params[key]
end
end
-
ActionController::Parameters.new(attrs).permit!
end
@@ -220,6 +244,44 @@ module API
error!({ 'message' => message }, status)
end
+ # Projects helpers
+
+ def filter_projects(projects)
+ # If the archived parameter is passed, limit results accordingly
+ if params[:archived].present?
+ projects = projects.where(archived: parse_boolean(params[:archived]))
+ end
+
+ if params[:search].present?
+ projects = projects.search(params[:search])
+ end
+
+ if params[:ci_enabled_first].present?
+ projects.includes(:gitlab_ci_service).
+ reorder("services.active DESC, projects.#{project_order_by} #{project_sort}")
+ else
+ projects.reorder(project_order_by => project_sort)
+ end
+ end
+
+ def project_order_by
+ order_fields = %w(id name path created_at updated_at last_activity_at)
+
+ if order_fields.include?(params['order_by'])
+ params['order_by']
+ else
+ 'created_at'
+ end
+ end
+
+ def project_sort
+ if params["sort"] == 'asc'
+ :asc
+ else
+ :desc
+ end
+ end
+
private
def add_pagination_headers(paginated, per_page)
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
new file mode 100644
index 00000000000..2b723b79504
--- /dev/null
+++ b/lib/api/keys.rb
@@ -0,0 +1,20 @@
+module API
+ # Keys API
+ class Keys < Grape::API
+ before { authenticate! }
+
+ resource :keys do
+ # Get single ssh key by id. Only available to admin users.
+ #
+ # Example Request:
+ # GET /keys/:id
+ get ":id" do
+ authenticated_as_admin!
+
+ key = Key.find(params[:id])
+
+ present key, with: Entities::SSHKeyWithUser
+ end
+ end
+ end
+end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 7412274b045..63ea2f05438 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -55,7 +55,7 @@ module API
else merge_requests
end
- merge_requests.reorder(issuable_order_by => issuable_sort)
+ merge_requests = merge_requests.reorder(issuable_order_by => issuable_sort)
present paginate(merge_requests), with: Entities::MergeRequest
end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index ad4d2e65dfd..882d1a083ad 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -44,7 +44,8 @@ module API
:issues_events,
:merge_requests_events,
:tag_push_events,
- :note_events
+ :note_events,
+ :enable_ssl_verification
]
@hook = user_project.hooks.new(attrs)
@@ -75,7 +76,8 @@ module API
:issues_events,
:merge_requests_events,
:tag_push_events,
- :note_events
+ :note_events,
+ :enable_ssl_verification
]
if @hook.update_attributes attrs
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 1f2251c9b9c..c2fb36b4143 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -11,42 +11,6 @@ module API
attrs[:visibility_level] = Gitlab::VisibilityLevel::PUBLIC if !attrs[:visibility_level].present? && publik == true
attrs
end
-
- def filter_projects(projects)
- # If the archived parameter is passed, limit results accordingly
- if params[:archived].present?
- projects = projects.where(archived: parse_boolean(params[:archived]))
- end
-
- if params[:search].present?
- projects = projects.search(params[:search])
- end
-
- if params[:ci_enabled_first].present?
- projects.includes(:gitlab_ci_service).
- reorder("services.active DESC, projects.#{project_order_by} #{project_sort}")
- else
- projects.reorder(project_order_by => project_sort)
- end
- end
-
- def project_order_by
- order_fields = %w(id name path created_at updated_at last_activity_at)
-
- if order_fields.include?(params['order_by'])
- params['order_by']
- else
- 'created_at'
- end
- end
-
- def project_sort
- if params["sort"] == 'asc'
- :asc
- else
- :desc
- end
- end
end
# Get a projects list for authenticated user
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 3ad59cf3adf..6727e80ac1e 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -4,74 +4,60 @@ module API
before { authenticate! }
before { authorize_admin_project }
+
resource :projects do
- # Set GitLab CI service for project
- #
- # Parameters:
- # token (required) - CI project token
- # project_url (required) - CI project url
+ # Set <service_slug> service for project
#
# Example Request:
+ #
# PUT /projects/:id/services/gitlab-ci
- put ":id/services/gitlab-ci" do
- required_attributes! [:token, :project_url]
- attrs = attributes_for_keys [:token, :project_url]
- user_project.build_missing_services
+ #
+ put ':id/services/:service_slug' do
+ if project_service
+ validators = project_service.class.validators.select do |s|
+ s.class == ActiveRecord::Validations::PresenceValidator &&
+ s.attributes != [:project_id]
+ end
+
+ required_attributes! validators.map(&:attributes).flatten.uniq
+ attrs = attributes_for_keys service_attributes
- if user_project.gitlab_ci_service.update_attributes(attrs.merge(active: true))
- true
- else
- not_found!
+ if project_service.update_attributes(attrs.merge(active: true))
+ true
+ else
+ not_found!
+ end
end
end
- # Delete GitLab CI service settings
+ # Delete <service_slug> service for project
#
# Example Request:
- # DELETE /projects/:id/services/gitlab-ci
- delete ":id/services/gitlab-ci" do
- if user_project.gitlab_ci_service
- user_project.gitlab_ci_service.update_attributes(
- active: false,
- token: nil,
- project_url: nil
- )
- end
- end
-
- # Set Hipchat service for project
#
- # Parameters:
- # token (required) - Hipchat token
- # room (required) - Hipchat room name
+ # DELETE /project/:id/services/gitlab-ci
#
- # Example Request:
- # PUT /projects/:id/services/hipchat
- put ':id/services/hipchat' do
- required_attributes! [:token, :room]
- attrs = attributes_for_keys [:token, :room]
- user_project.build_missing_services
+ delete ':id/services/:service_slug' do
+ if project_service
+ attrs = service_attributes.inject({}) do |hash, key|
+ hash.merge!(key => nil)
+ end
- if user_project.hipchat_service.update_attributes(
- attrs.merge(active: true))
- true
- else
- not_found!
+ if project_service.update_attributes(attrs.merge(active: false))
+ true
+ else
+ not_found!
+ end
end
end
- # Delete Hipchat service settings
+ # Get <service_slug> service settings for project
#
# Example Request:
- # DELETE /projects/:id/services/hipchat
- delete ':id/services/hipchat' do
- if user_project.hipchat_service
- user_project.hipchat_service.update_attributes(
- active: false,
- token: nil,
- room: nil
- )
- end
+ #
+ # GET /project/:id/services/gitlab-ci
+ #
+ get ':id/services/:service_slug' do
+ present project_service
end
end
end
diff --git a/lib/api/users.rb b/lib/api/users.rb
index ee29f952246..a98d668e02d 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -3,7 +3,7 @@ module API
class Users < Grape::API
before { authenticate! }
- resource :users do
+ resource :users, requirements: { uid: /[0-9]*/, id: /[0-9]*/ } do
# Get a users list
#
# Example Request:
@@ -121,6 +121,17 @@ module API
User.where(username: attrs[:username]).
where.not(id: user.id).count > 0
+ identity_attrs = attributes_for_keys [:provider, :extern_uid]
+ if identity_attrs.any?
+ identity = user.identities.find_by(provider: identity_attrs[:provider])
+ if identity
+ identity.update_attributes(identity_attrs)
+ else
+ identity = user.identities.build(identity_attrs)
+ identity.save
+ end
+ end
+
if user.update_attributes(attrs)
present user, with: Entities::UserFull
else
diff --git a/lib/backup/builds.rb b/lib/backup/builds.rb
new file mode 100644
index 00000000000..6f56f680bb9
--- /dev/null
+++ b/lib/backup/builds.rb
@@ -0,0 +1,34 @@
+module Backup
+ class Builds
+ attr_reader :app_builds_dir, :backup_builds_dir, :backup_dir
+
+ def initialize
+ @app_builds_dir = Settings.gitlab_ci.builds_path
+ @backup_dir = Gitlab.config.backup.path
+ @backup_builds_dir = File.join(Gitlab.config.backup.path, 'builds')
+ end
+
+ # Copy builds from builds directory to backup/builds
+ def dump
+ FileUtils.rm_rf(backup_builds_dir)
+ # Ensure the parent dir of backup_builds_dir exists
+ FileUtils.mkdir_p(Gitlab.config.backup.path)
+ # Fail if somebody raced to create backup_builds_dir before us
+ FileUtils.mkdir(backup_builds_dir, mode: 0700)
+ FileUtils.cp_r(app_builds_dir, backup_dir)
+ end
+
+ def restore
+ backup_existing_builds_dir
+
+ FileUtils.cp_r(backup_builds_dir, app_builds_dir)
+ end
+
+ def backup_existing_builds_dir
+ timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}")
+ if File.exists?(app_builds_dir)
+ FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path))
+ end
+ end
+ end
+end
diff --git a/lib/backup/database.rb b/lib/backup/database.rb
index ce75476a09b..959ac4b7868 100644
--- a/lib/backup/database.rb
+++ b/lib/backup/database.rb
@@ -25,8 +25,12 @@ module Backup
when "postgresql" then
$progress.print "Dumping PostgreSQL database #{config['database']} ... "
pg_env
- # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
- system('pg_dump', '--clean', config['database'], out: db_file_name)
+ pgsql_args = ["--clean"] # Pass '--clean' to include 'DROP TABLE' statements in the DB dump.
+ if Gitlab.config.backup.pg_schema
+ pgsql_args << "-n"
+ pgsql_args << Gitlab.config.backup.pg_schema
+ end
+ system('pg_dump', *pgsql_args, config['database'], out: db_file_name)
end
report_success(success)
abort 'Backup failed' unless success
diff --git a/lib/backup/manager.rb b/lib/backup/manager.rb
index 13c68d9354f..5c42f25f4a2 100644
--- a/lib/backup/manager.rb
+++ b/lib/backup/manager.rb
@@ -45,7 +45,8 @@ module Backup
directory = connection.directories.get(remote_directory)
if directory.files.create(key: tar_file, body: File.open(tar_file), public: false,
- multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size)
+ multipart_chunk_size: Gitlab.config.backup.upload.multipart_chunk_size,
+ encryption: Gitlab.config.backup.upload.encryption)
$progress.puts "done".green
else
puts "uploading backup to #{remote_directory} failed".red
@@ -55,7 +56,7 @@ module Backup
def cleanup
$progress.print "Deleting tmp directories ... "
-
+
backup_contents.each do |dir|
next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
@@ -75,7 +76,7 @@ module Backup
if keep_time > 0
removed = 0
-
+
Dir.chdir(Gitlab.config.backup.path) do
file_list = Dir.glob('*_gitlab_backup.tar')
file_list.map! { |f| $1.to_i if f =~ /(\d+)_gitlab_backup.tar/ }
@@ -153,7 +154,7 @@ module Backup
end
def folders_to_backup
- folders = %w{repositories db uploads}
+ folders = %w{repositories db uploads builds}
if ENV["SKIP"]
return folders.reject{ |folder| ENV["SKIP"].include?(folder) }
diff --git a/lib/ci/ansi2html.rb b/lib/ci/ansi2html.rb
new file mode 100644
index 00000000000..ac6d667cf8d
--- /dev/null
+++ b/lib/ci/ansi2html.rb
@@ -0,0 +1,224 @@
+# ANSI color library
+#
+# Implementation per http://en.wikipedia.org/wiki/ANSI_escape_code
+module Ci
+ module Ansi2html
+ # keys represent the trailing digit in color changing command (30-37, 40-47, 90-97. 100-107)
+ COLOR = {
+ 0 => 'black', # not that this is gray in the intense color table
+ 1 => 'red',
+ 2 => 'green',
+ 3 => 'yellow',
+ 4 => 'blue',
+ 5 => 'magenta',
+ 6 => 'cyan',
+ 7 => 'white', # not that this is gray in the dark (aka default) color table
+ }
+
+ STYLE_SWITCHES = {
+ bold: 0x01,
+ italic: 0x02,
+ underline: 0x04,
+ conceal: 0x08,
+ cross: 0x10,
+ }
+
+ def self.convert(ansi)
+ Converter.new().convert(ansi)
+ end
+
+ class Converter
+ def on_0(s) reset() end
+ def on_1(s) enable(STYLE_SWITCHES[:bold]) end
+ def on_3(s) enable(STYLE_SWITCHES[:italic]) end
+ def on_4(s) enable(STYLE_SWITCHES[:underline]) end
+ def on_8(s) enable(STYLE_SWITCHES[:conceal]) end
+ def on_9(s) enable(STYLE_SWITCHES[:cross]) end
+
+ def on_21(s) disable(STYLE_SWITCHES[:bold]) end
+ def on_22(s) disable(STYLE_SWITCHES[:bold]) end
+ def on_23(s) disable(STYLE_SWITCHES[:italic]) end
+ def on_24(s) disable(STYLE_SWITCHES[:underline]) end
+ def on_28(s) disable(STYLE_SWITCHES[:conceal]) end
+ def on_29(s) disable(STYLE_SWITCHES[:cross]) end
+
+ def on_30(s) set_fg_color(0) end
+ def on_31(s) set_fg_color(1) end
+ def on_32(s) set_fg_color(2) end
+ def on_33(s) set_fg_color(3) end
+ def on_34(s) set_fg_color(4) end
+ def on_35(s) set_fg_color(5) end
+ def on_36(s) set_fg_color(6) end
+ def on_37(s) set_fg_color(7) end
+ def on_38(s) set_fg_color_256(s) end
+ def on_39(s) set_fg_color(9) end
+
+ def on_40(s) set_bg_color(0) end
+ def on_41(s) set_bg_color(1) end
+ def on_42(s) set_bg_color(2) end
+ def on_43(s) set_bg_color(3) end
+ def on_44(s) set_bg_color(4) end
+ def on_45(s) set_bg_color(5) end
+ def on_46(s) set_bg_color(6) end
+ def on_47(s) set_bg_color(7) end
+ def on_48(s) set_bg_color_256(s) end
+ def on_49(s) set_bg_color(9) end
+
+ def on_90(s) set_fg_color(0, 'l') end
+ def on_91(s) set_fg_color(1, 'l') end
+ def on_92(s) set_fg_color(2, 'l') end
+ def on_93(s) set_fg_color(3, 'l') end
+ def on_94(s) set_fg_color(4, 'l') end
+ def on_95(s) set_fg_color(5, 'l') end
+ def on_96(s) set_fg_color(6, 'l') end
+ def on_97(s) set_fg_color(7, 'l') end
+ def on_99(s) set_fg_color(9, 'l') end
+
+ def on_100(s) set_bg_color(0, 'l') end
+ def on_101(s) set_bg_color(1, 'l') end
+ def on_102(s) set_bg_color(2, 'l') end
+ def on_103(s) set_bg_color(3, 'l') end
+ def on_104(s) set_bg_color(4, 'l') end
+ def on_105(s) set_bg_color(5, 'l') end
+ def on_106(s) set_bg_color(6, 'l') end
+ def on_107(s) set_bg_color(7, 'l') end
+ def on_109(s) set_bg_color(9, 'l') end
+
+ def convert(ansi)
+ @out = ""
+ @n_open_tags = 0
+ reset()
+
+ s = StringScanner.new(ansi.gsub("<", "&lt;"))
+ while(!s.eos?)
+ if s.scan(/\e([@-_])(.*?)([@-~])/)
+ handle_sequence(s)
+ else
+ @out << s.scan(/./m)
+ end
+ end
+
+ close_open_tags()
+ @out
+ end
+
+ def handle_sequence(s)
+ indicator = s[1]
+ commands = s[2].split ';'
+ terminator = s[3]
+
+ # We are only interested in color and text style changes - triggered by
+ # sequences starting with '\e[' and ending with 'm'. Any other control
+ # sequence gets stripped (including stuff like "delete last line")
+ return unless indicator == '[' and terminator == 'm'
+
+ close_open_tags()
+
+ if commands.empty?()
+ reset()
+ return
+ end
+
+ evaluate_command_stack(commands)
+
+ css_classes = []
+
+ unless @fg_color.nil?
+ fg_color = @fg_color
+ # Most terminals show bold colored text in the light color variant
+ # Let's mimic that here
+ if @style_mask & STYLE_SWITCHES[:bold] != 0
+ fg_color.sub!(/fg-(\w{2,}+)/, 'fg-l-\1')
+ end
+ css_classes << fg_color
+ end
+ css_classes << @bg_color unless @bg_color.nil?
+
+ STYLE_SWITCHES.each do |css_class, flag|
+ css_classes << "term-#{css_class}" if @style_mask & flag != 0
+ end
+
+ open_new_tag(css_classes) if css_classes.length > 0
+ end
+
+ def evaluate_command_stack(stack)
+ return unless command = stack.shift()
+
+ if self.respond_to?("on_#{command}", true)
+ self.send("on_#{command}", stack)
+ end
+
+ evaluate_command_stack(stack)
+ end
+
+ def open_new_tag(css_classes)
+ @out << %{<span class="#{css_classes.join(' ')}">}
+ @n_open_tags += 1
+ end
+
+ def close_open_tags
+ while @n_open_tags > 0
+ @out << %{</span>}
+ @n_open_tags -= 1
+ end
+ end
+
+ def reset
+ @fg_color = nil
+ @bg_color = nil
+ @style_mask = 0
+ end
+
+ def enable(flag)
+ @style_mask |= flag
+ end
+
+ def disable(flag)
+ @style_mask &= ~flag
+ end
+
+ def set_fg_color(color_index, prefix = nil)
+ @fg_color = get_term_color_class(color_index, ["fg", prefix])
+ end
+
+ def set_bg_color(color_index, prefix = nil)
+ @bg_color = get_term_color_class(color_index, ["bg", prefix])
+ end
+
+ def get_term_color_class(color_index, prefix)
+ color_name = COLOR[color_index]
+ return nil if color_name.nil?
+
+ get_color_class(["term", prefix, color_name])
+ end
+
+ def set_fg_color_256(command_stack)
+ css_class = get_xterm_color_class(command_stack, "fg")
+ @fg_color = css_class unless css_class.nil?
+ end
+
+ def set_bg_color_256(command_stack)
+ css_class = get_xterm_color_class(command_stack, "bg")
+ @bg_color = css_class unless css_class.nil?
+ end
+
+ def get_xterm_color_class(command_stack, prefix)
+ # the 38 and 48 commands have to be followed by "5" and the color index
+ return unless command_stack.length >= 2
+ return unless command_stack[0] == "5"
+
+ command_stack.shift() # ignore the "5" command
+ color_index = command_stack.shift().to_i
+
+ return unless color_index >= 0
+ return unless color_index <= 255
+
+ get_color_class(["xterm", prefix, color_index])
+ end
+
+ def get_color_class(segments)
+ [segments].flatten.compact.join('-')
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
new file mode 100644
index 00000000000..218d8c3adcc
--- /dev/null
+++ b/lib/ci/api/api.rb
@@ -0,0 +1,38 @@
+Dir["#{Rails.root}/lib/ci/api/*.rb"].each {|file| require file}
+
+module Ci
+ module API
+ class API < Grape::API
+ include APIGuard
+ version 'v1', using: :path
+
+ rescue_from ActiveRecord::RecordNotFound do
+ rack_response({ 'message' => '404 Not found' }.to_json, 404)
+ end
+
+ rescue_from :all do |exception|
+ # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
+ # why is this not wrapped in something reusable?
+ trace = exception.backtrace
+
+ message = "\n#{exception.class} (#{exception.message}):\n"
+ message << exception.annoted_source_code.to_s if exception.respond_to?(:annoted_source_code)
+ message << " " << trace.join("\n ")
+
+ API.logger.add Logger::FATAL, message
+ rack_response({ 'message' => '500 Internal Server Error' }, 500)
+ end
+
+ format :json
+
+ helpers Helpers
+ helpers ::API::APIHelpers
+
+ mount Builds
+ mount Commits
+ mount Runners
+ mount Projects
+ mount Triggers
+ end
+ end
+end
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
new file mode 100644
index 00000000000..83ca1e6481c
--- /dev/null
+++ b/lib/ci/api/builds.rb
@@ -0,0 +1,53 @@
+module Ci
+ module API
+ # Builds API
+ class Builds < Grape::API
+ resource :builds do
+ # Runs oldest pending build by runner - Runners only
+ #
+ # Parameters:
+ # token (required) - The uniq token of runner
+ #
+ # Example Request:
+ # POST /builds/register
+ post "register" do
+ authenticate_runner!
+ update_runner_last_contact
+ required_attributes! [:token]
+ not_found! unless current_runner.active?
+
+ build = Ci::RegisterBuildService.new.execute(current_runner)
+
+ if build
+ update_runner_info
+ present build, with: Entities::Build
+ else
+ not_found!
+ end
+ end
+
+ # Update an existing build - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # state (optional) - The state of a build
+ # trace (optional) - The trace of a build
+ # Example Request:
+ # PUT /builds/:id
+ put ":id" do
+ authenticate_runner!
+ update_runner_last_contact
+ build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
+ build.update_attributes(trace: params[:trace]) if params[:trace]
+
+ case params[:state].to_s
+ when 'success'
+ build.success
+ when 'failed'
+ build.drop
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/commits.rb b/lib/ci/api/commits.rb
new file mode 100644
index 00000000000..a60769d8305
--- /dev/null
+++ b/lib/ci/api/commits.rb
@@ -0,0 +1,66 @@
+module Ci
+ module API
+ class Commits < Grape::API
+ resource :commits do
+ # Get list of commits per project
+ #
+ # Parameters:
+ # project_id (required) - The ID of a project
+ # project_token (requires) - Project token
+ # page (optional)
+ # per_page (optional) - items per request (default is 20)
+ #
+ get do
+ required_attributes! [:project_id, :project_token]
+ project = Ci::Project.find(params[:project_id])
+ authenticate_project_token!(project)
+
+ commits = project.commits.page(params[:page]).per(params[:per_page] || 20)
+ present commits, with: Entities::CommitWithBuilds
+ end
+
+ # Create a commit
+ #
+ # Parameters:
+ # project_id (required) - The ID of a project
+ # project_token (requires) - Project token
+ # data (required) - GitLab push data
+ #
+ # Sample GitLab push data:
+ # {
+ # "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ # "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ # "ref": "refs/heads/master",
+ # "commits": [
+ # {
+ # "id": "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ # "message": "Update Catalan translation to e38cb41.",
+ # "timestamp": "2011-12-12T14:27:31+02:00",
+ # "url": "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ # "author": {
+ # "name": "Jordi Mallach",
+ # "email": "jordi@softcatala.org",
+ # }
+ # }, .... more commits
+ # ]
+ # }
+ #
+ # Example Request:
+ # POST /commits
+ post do
+ required_attributes! [:project_id, :data, :project_token]
+ project = Ci::Project.find(params[:project_id])
+ authenticate_project_token!(project)
+ commit = Ci::CreateCommitService.new.execute(project, current_user, params[:data])
+
+ if commit.persisted?
+ present commit, with: Entities::CommitWithBuilds
+ else
+ errors = commit.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/entities.rb b/lib/ci/api/entities.rb
new file mode 100644
index 00000000000..f47bc1236b8
--- /dev/null
+++ b/lib/ci/api/entities.rb
@@ -0,0 +1,56 @@
+module Ci
+ module API
+ module Entities
+ class Commit < Grape::Entity
+ expose :id, :ref, :sha, :project_id, :before_sha, :created_at
+ expose :status, :finished_at, :duration
+ expose :git_commit_message, :git_author_name, :git_author_email
+ end
+
+ class CommitWithBuilds < Commit
+ expose :builds
+ end
+
+ class Build < Grape::Entity
+ expose :id, :commands, :ref, :sha, :project_id, :repo_url,
+ :before_sha, :allow_git_fetch, :project_name
+
+ expose :options do |model|
+ model.options
+ end
+
+ expose :timeout do |model|
+ model.timeout
+ end
+
+ expose :variables
+ end
+
+ class Runner < Grape::Entity
+ expose :id, :token
+ end
+
+ class Project < Grape::Entity
+ expose :id, :name, :token, :default_ref, :gitlab_url, :path,
+ :always_build, :polling_interval, :public, :ssh_url_to_repo, :gitlab_id
+
+ expose :timeout do |model|
+ model.timeout
+ end
+ end
+
+ class RunnerProject < Grape::Entity
+ expose :id, :project_id, :runner_id
+ end
+
+ class WebHook < Grape::Entity
+ expose :id, :project_id, :url
+ end
+
+ class TriggerRequest < Grape::Entity
+ expose :id, :variables
+ expose :commit, using: Commit
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
new file mode 100644
index 00000000000..e602cda81d6
--- /dev/null
+++ b/lib/ci/api/helpers.rb
@@ -0,0 +1,35 @@
+module Ci
+ module API
+ module Helpers
+ UPDATE_RUNNER_EVERY = 60
+
+ def authenticate_runners!
+ forbidden! unless params[:token] == GitlabCi::REGISTRATION_TOKEN
+ end
+
+ def authenticate_runner!
+ forbidden! unless current_runner
+ end
+
+ def authenticate_project_token!(project)
+ forbidden! unless project.valid_token?(params[:project_token])
+ end
+
+ def update_runner_last_contact
+ if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= UPDATE_RUNNER_EVERY
+ current_runner.update_attributes(contacted_at: Time.now)
+ end
+ end
+
+ def current_runner
+ @runner ||= Runner.find_by_token(params[:token].to_s)
+ end
+
+ def update_runner_info
+ return unless params["info"].present?
+ info = attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
+ current_runner.update(info)
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/projects.rb b/lib/ci/api/projects.rb
new file mode 100644
index 00000000000..d719ad9e8d5
--- /dev/null
+++ b/lib/ci/api/projects.rb
@@ -0,0 +1,195 @@
+module Ci
+ module API
+ # Projects API
+ class Projects < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Register new webhook for project
+ #
+ # Parameters
+ # project_id (required) - The ID of a project
+ # web_hook (required) - WebHook URL
+ # Example Request
+ # POST /projects/:project_id/webhooks
+ post ":project_id/webhooks" do
+ required_attributes! [:web_hook]
+
+ project = Ci::Project.find(params[:project_id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ web_hook = project.web_hooks.new({ url: params[:web_hook] })
+
+ if web_hook.save
+ present web_hook, with: Entities::WebHook
+ else
+ errors = web_hook.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Retrieve all Gitlab CI projects that the user has access to
+ #
+ # Example Request:
+ # GET /projects
+ get do
+ gitlab_projects = current_user.authorized_projects
+ gitlab_projects = filter_projects(gitlab_projects)
+ gitlab_projects = paginate gitlab_projects
+
+ ids = gitlab_projects.map { |project| project.id }
+
+ projects = Ci::Project.where("gitlab_id IN (?)", ids).load
+ present projects, with: Entities::Project
+ end
+
+ # Retrieve all Gitlab CI projects that the user owns
+ #
+ # Example Request:
+ # GET /projects/owned
+ get "owned" do
+ gitlab_projects = current_user.owned_projects
+ gitlab_projects = filter_projects(gitlab_projects)
+ gitlab_projects = paginate gitlab_projects
+
+ ids = gitlab_projects.map { |project| project.id }
+
+ projects = Ci::Project.where("gitlab_id IN (?)", ids).load
+ present projects, with: Entities::Project
+ end
+
+ # Retrieve info for a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # GET /projects/:id
+ get ":id" do
+ project = Ci::Project.find(params[:id])
+ unauthorized! unless can?(current_user, :read_project, project.gl_project)
+
+ present project, with: Entities::Project
+ end
+
+ # Create Gitlab CI project using Gitlab project info
+ #
+ # Parameters:
+ # gitlab_id (required) - The gitlab id of the project
+ # default_ref - The branch to run against (defaults to `master`)
+ # Example Request:
+ # POST /projects
+ post do
+ required_attributes! [:gitlab_id]
+
+ filtered_params = {
+ gitlab_id: params[:gitlab_id],
+ # we accept gitlab_url for backward compatibility for a while (added to 7.11)
+ default_ref: params[:default_ref] || 'master'
+ }
+
+ project = Ci::Project.new(filtered_params)
+ project.build_missing_services
+
+ if project.save
+ present project, with: Entities::Project
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Update a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # default_ref - The branch to run against (defaults to `master`)
+ # Example Request:
+ # PUT /projects/:id
+ put ":id" do
+ project = Ci::Project.find(params[:id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ attrs = attributes_for_keys [:default_ref]
+
+ if project.update_attributes(attrs)
+ present project, with: Entities::Project
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Remove a Gitlab CI project
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # Example Request:
+ # DELETE /projects/:id
+ delete ":id" do
+ project = Ci::Project.find(params[:id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ project.destroy
+ end
+
+ # Link a Gitlab CI project to a runner
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # runner_id (required) - The ID of a runner
+ # Example Request:
+ # POST /projects/:id/runners/:runner_id
+ post ":id/runners/:runner_id" do
+ project = Ci::Project.find(params[:id])
+ runner = Ci::Runner.find(params[:runner_id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ options = {
+ project_id: project.id,
+ runner_id: runner.id
+ }
+
+ runner_project = Ci::RunnerProject.new(options)
+
+ if runner_project.save
+ present runner_project, with: Entities::RunnerProject
+ else
+ errors = project.errors.full_messages.join(", ")
+ render_api_error!(errors, 400)
+ end
+ end
+
+ # Remove a Gitlab CI project from a runner
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # runner_id (required) - The ID of a runner
+ # Example Request:
+ # DELETE /projects/:id/runners/:runner_id
+ delete ":id/runners/:runner_id" do
+ project = Ci::Project.find(params[:id])
+ runner = Ci::Runner.find(params[:runner_id])
+
+ unauthorized! unless can?(current_user, :admin_project, project.gl_project)
+
+ options = {
+ project_id: project.id,
+ runner_id: runner.id
+ }
+
+ runner_project = Ci::RunnerProject.find_by(options)
+
+ if runner_project.present?
+ runner_project.destroy
+ else
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
new file mode 100644
index 00000000000..1466fe4356e
--- /dev/null
+++ b/lib/ci/api/runners.rb
@@ -0,0 +1,69 @@
+module Ci
+ module API
+ # Runners API
+ class Runners < Grape::API
+ resource :runners do
+ # Get list of all available runners
+ #
+ # Example Request:
+ # GET /runners
+ get do
+ authenticate!
+ runners = Ci::Runner.all
+
+ present runners, with: Entities::Runner
+ end
+
+ # Delete runner
+ # Parameters:
+ # token (required) - The unique token of runner
+ #
+ # Example Request:
+ # GET /runners/delete
+ delete "delete" do
+ required_attributes! [:token]
+ authenticate_runner!
+ Ci::Runner.find_by_token(params[:token]).destroy
+ end
+
+ # Register a new runner
+ #
+ # Note: This is an "internal" API called when setting up
+ # runners, so it is authenticated differently.
+ #
+ # Parameters:
+ # token (required) - The unique token of runner
+ #
+ # Example Request:
+ # POST /runners/register
+ post "register" do
+ required_attributes! [:token]
+
+ runner =
+ if params[:token] == GitlabCi::REGISTRATION_TOKEN
+ # Create shared runner. Requires admin access
+ Ci::Runner.create(
+ description: params[:description],
+ tag_list: params[:tag_list],
+ is_shared: true
+ )
+ elsif project = Ci::Project.find_by(token: params[:token])
+ # Create a specific runner for project.
+ project.runners.create(
+ description: params[:description],
+ tag_list: params[:tag_list]
+ )
+ end
+
+ return forbidden! unless runner
+
+ if runner.id
+ present runner, with: Entities::Runner
+ else
+ not_found!
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb
new file mode 100644
index 00000000000..40907d6db54
--- /dev/null
+++ b/lib/ci/api/triggers.rb
@@ -0,0 +1,49 @@
+module Ci
+ module API
+ # Build Trigger API
+ class Triggers < Grape::API
+ resource :projects do
+ # Trigger a GitLab CI project build
+ #
+ # Parameters:
+ # id (required) - The ID of a CI project
+ # ref (required) - The name of project's branch or tag
+ # token (required) - The uniq token of trigger
+ # Example Request:
+ # POST /projects/:id/ref/:ref/trigger
+ post ":id/refs/:ref/trigger" do
+ required_attributes! [:token]
+
+ project = Ci::Project.find(params[:id])
+ trigger = Ci::Trigger.find_by_token(params[:token].to_s)
+ not_found! unless project && trigger
+ unauthorized! unless trigger.project == project
+
+ # validate variables
+ variables = params[:variables]
+ if variables
+ unless variables.is_a?(Hash)
+ render_api_error!('variables needs to be a hash', 400)
+ end
+
+ unless variables.all? { |key, value| key.is_a?(String) && value.is_a?(String) }
+ render_api_error!('variables needs to be a map of key-valued strings', 400)
+ end
+
+ # convert variables from Mash to Hash
+ variables = variables.to_h
+ end
+
+ # create request and trigger builds
+ trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
+ if trigger_request
+ present trigger_request, with: Entities::TriggerRequest
+ else
+ errors = 'No builds created'
+ render_api_error!(errors, 400)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/assets/.gitkeep b/lib/ci/assets/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/lib/ci/assets/.gitkeep
diff --git a/lib/ci/charts.rb b/lib/ci/charts.rb
new file mode 100644
index 00000000000..915a4f526a6
--- /dev/null
+++ b/lib/ci/charts.rb
@@ -0,0 +1,71 @@
+module Ci
+ module Charts
+ class Chart
+ attr_reader :labels, :total, :success, :project, :build_times
+
+ def initialize(project)
+ @labels = []
+ @total = []
+ @success = []
+ @build_times = []
+ @project = project
+
+ collect
+ end
+
+
+ def push(from, to, format)
+ @labels << from.strftime(format)
+ @total << project.builds.
+ where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from).
+ count(:all)
+ @success << project.builds.
+ where("? > #{Ci::Build.table_name}.created_at AND #{Ci::Build.table_name}.created_at > ?", to, from).
+ success.count(:all)
+ end
+ end
+
+ class YearChart < Chart
+ def collect
+ 13.times do |i|
+ start_month = (Date.today.years_ago(1) + i.month).beginning_of_month
+ end_month = start_month.end_of_month
+
+ push(start_month, end_month, "%d %B %Y")
+ end
+ end
+ end
+
+ class MonthChart < Chart
+ def collect
+ 30.times do |i|
+ start_day = Date.today - 30.days + i.days
+ end_day = Date.today - 30.days + i.day + 1.day
+
+ push(start_day, end_day, "%d %B")
+ end
+ end
+ end
+
+ class WeekChart < Chart
+ def collect
+ 7.times do |i|
+ start_day = Date.today - 7.days + i.days
+ end_day = Date.today - 7.days + i.day + 1.day
+
+ push(start_day, end_day, "%d %B")
+ end
+ end
+ end
+
+ class BuildTime < Chart
+ def collect
+ commits = project.commits.joins(:builds).where("#{Ci::Build.table_name}.finished_at is NOT NULL AND #{Ci::Build.table_name}.started_at is NOT NULL").last(30)
+ commits.each do |commit|
+ @labels << commit.short_sha
+ @build_times << (commit.duration / 60)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/current_settings.rb b/lib/ci/current_settings.rb
new file mode 100644
index 00000000000..fd78b024970
--- /dev/null
+++ b/lib/ci/current_settings.rb
@@ -0,0 +1,22 @@
+module Ci
+ module CurrentSettings
+ def current_application_settings
+ key = :ci_current_application_settings
+
+ RequestStore.store[key] ||= begin
+ if ActiveRecord::Base.connected? && ActiveRecord::Base.connection.table_exists?('ci_application_settings')
+ Ci::ApplicationSetting.current || Ci::ApplicationSetting.create_from_defaults
+ else
+ fake_application_settings
+ end
+ end
+ end
+
+ def fake_application_settings
+ OpenStruct.new(
+ all_broken_builds: Ci::Settings.gitlab_ci['all_broken_builds'],
+ add_pusher: Ci::Settings.gitlab_ci['add_pusher'],
+ )
+ end
+ end
+end
diff --git a/lib/ci/git.rb b/lib/ci/git.rb
new file mode 100644
index 00000000000..7acc3f38edb
--- /dev/null
+++ b/lib/ci/git.rb
@@ -0,0 +1,5 @@
+module Ci
+ module Git
+ BLANK_SHA = '0' * 40
+ end
+end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
new file mode 100644
index 00000000000..c47951bc5d1
--- /dev/null
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -0,0 +1,199 @@
+module Ci
+ class GitlabCiYamlProcessor
+ class ValidationError < StandardError;end
+
+ DEFAULT_STAGES = %w(build test deploy)
+ DEFAULT_STAGE = 'test'
+ ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables]
+ ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services, :allow_failure, :type, :stage]
+
+ attr_reader :before_script, :image, :services, :variables
+
+ def initialize(config)
+ @config = YAML.load(config)
+
+ unless @config.is_a? Hash
+ raise ValidationError, "YAML should be a hash"
+ end
+
+ @config = @config.deep_symbolize_keys
+
+ initial_parsing
+
+ validate!
+ end
+
+ def builds_for_stage_and_ref(stage, ref, tag = false)
+ builds.select{|build| build[:stage] == stage && process?(build[:only], build[:except], ref, tag)}
+ end
+
+ def builds
+ @jobs.map do |name, job|
+ build_job(name, job)
+ end
+ end
+
+ def stages
+ @stages || DEFAULT_STAGES
+ end
+
+ private
+
+ def initial_parsing
+ @before_script = @config[:before_script] || []
+ @image = @config[:image]
+ @services = @config[:services]
+ @stages = @config[:stages] || @config[:types]
+ @variables = @config[:variables] || {}
+ @config.except!(*ALLOWED_YAML_KEYS)
+
+ # anything that doesn't have script is considered as unknown
+ @config.each do |name, param|
+ raise ValidationError, "Unknown parameter: #{name}" unless param.is_a?(Hash) && param.has_key?(:script)
+ end
+
+ unless @config.values.any?{|job| job.is_a?(Hash)}
+ raise ValidationError, "Please define at least one job"
+ end
+
+ @jobs = {}
+ @config.each do |key, job|
+ stage = job[:stage] || job[:type] || DEFAULT_STAGE
+ @jobs[key] = { stage: stage }.merge(job)
+ end
+ end
+
+ def process?(only_params, except_params, ref, tag)
+ return true if only_params.nil? && except_params.nil?
+
+ if only_params
+ return true if tag && only_params.include?("tags")
+ return true if !tag && only_params.include?("branches")
+
+ only_params.find do |pattern|
+ match_ref?(pattern, ref)
+ end
+ else
+ return false if tag && except_params.include?("tags")
+ return false if !tag && except_params.include?("branches")
+
+ except_params.each do |pattern|
+ return false if match_ref?(pattern, ref)
+ end
+ end
+ end
+
+ def build_job(name, job)
+ {
+ stage_idx: stages.index(job[:stage]),
+ stage: job[:stage],
+ commands: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
+ tag_list: job[:tags] || [],
+ name: name,
+ only: job[:only],
+ except: job[:except],
+ allow_failure: job[:allow_failure] || false,
+ options: {
+ image: job[:image] || @image,
+ services: job[:services] || @services
+ }.compact
+ }
+ end
+
+ def match_ref?(pattern, ref)
+ if pattern.first == "/" && pattern.last == "/"
+ Regexp.new(pattern[1...-1]) =~ ref
+ else
+ pattern == ref
+ end
+ end
+
+ def normalize_script(script)
+ if script.is_a? Array
+ script.join("\n")
+ else
+ script
+ end
+ end
+
+ def validate!
+ unless validate_array_of_strings(@before_script)
+ raise ValidationError, "before_script should be an array of strings"
+ end
+
+ unless @image.nil? || @image.is_a?(String)
+ raise ValidationError, "image should be a string"
+ end
+
+ unless @services.nil? || validate_array_of_strings(@services)
+ raise ValidationError, "services should be an array of strings"
+ end
+
+ unless @stages.nil? || validate_array_of_strings(@stages)
+ raise ValidationError, "stages should be an array of strings"
+ end
+
+ unless @variables.nil? || validate_variables(@variables)
+ raise ValidationError, "variables should be a map of key-valued strings"
+ end
+
+ @jobs.each do |name, job|
+ validate_job!("#{name} job", job)
+ end
+
+ true
+ end
+
+ def validate_job!(name, job)
+ job.keys.each do |key|
+ unless ALLOWED_JOB_KEYS.include? key
+ raise ValidationError, "#{name}: unknown parameter #{key}"
+ end
+ end
+
+ if !job[:script].is_a?(String) && !validate_array_of_strings(job[:script])
+ raise ValidationError, "#{name}: script should be a string or an array of a strings"
+ end
+
+ if job[:stage]
+ unless job[:stage].is_a?(String) && job[:stage].in?(stages)
+ raise ValidationError, "#{name}: stage parameter should be #{stages.join(", ")}"
+ end
+ end
+
+ if job[:image] && !job[:image].is_a?(String)
+ raise ValidationError, "#{name}: image should be a string"
+ end
+
+ if job[:services] && !validate_array_of_strings(job[:services])
+ raise ValidationError, "#{name}: services should be an array of strings"
+ end
+
+ if job[:tags] && !validate_array_of_strings(job[:tags])
+ raise ValidationError, "#{name}: tags parameter should be an array of strings"
+ end
+
+ if job[:only] && !validate_array_of_strings(job[:only])
+ raise ValidationError, "#{name}: only parameter should be an array of strings"
+ end
+
+ if job[:except] && !validate_array_of_strings(job[:except])
+ raise ValidationError, "#{name}: except parameter should be an array of strings"
+ end
+
+ if job[:allow_failure] && !job[:allow_failure].in?([true, false])
+ raise ValidationError, "#{name}: allow_failure parameter should be an boolean"
+ end
+ end
+
+ private
+
+ def validate_array_of_strings(values)
+ values.is_a?(Array) && values.all? {|tag| tag.is_a?(String)}
+ end
+
+ def validate_variables(variables)
+ variables.is_a?(Hash) && variables.all? {|key, value| key.is_a?(Symbol) && value.is_a?(String)}
+ end
+ end
+end
diff --git a/lib/ci/migrate/builds.rb b/lib/ci/migrate/builds.rb
new file mode 100644
index 00000000000..c4f62e55295
--- /dev/null
+++ b/lib/ci/migrate/builds.rb
@@ -0,0 +1,29 @@
+module Ci
+ module Migrate
+ class Builds
+ attr_reader :app_builds_dir, :backup_builds_tarball, :backup_dir
+
+ def initialize
+ @app_builds_dir = Settings.gitlab_ci.builds_path
+ @backup_dir = Gitlab.config.backup.path
+ @backup_builds_tarball = File.join(backup_dir, 'builds/builds.tar.gz')
+ end
+
+ def restore
+ backup_existing_builds_dir
+
+ FileUtils.mkdir_p(app_builds_dir, mode: 0700)
+ unless system('tar', '-C', app_builds_dir, '-zxf', backup_builds_tarball)
+ abort 'Restore failed'.red
+ end
+ end
+
+ def backup_existing_builds_dir
+ timestamped_builds_path = File.join(app_builds_dir, '..', "builds.#{Time.now.to_i}")
+ if File.exists?(app_builds_dir)
+ FileUtils.mv(app_builds_dir, File.expand_path(timestamped_builds_path))
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/migrate/database.rb b/lib/ci/migrate/database.rb
new file mode 100644
index 00000000000..bf9b80f1f62
--- /dev/null
+++ b/lib/ci/migrate/database.rb
@@ -0,0 +1,67 @@
+require 'yaml'
+
+module Ci
+ module Migrate
+ class Database
+ attr_reader :config
+
+ def initialize
+ @config = YAML.load_file(File.join(Rails.root, 'config', 'database.yml'))[Rails.env]
+ end
+
+ def restore
+ decompress_rd, decompress_wr = IO.pipe
+ decompress_pid = spawn(*%W(gzip -cd), out: decompress_wr, in: db_file_name)
+ decompress_wr.close
+
+ restore_pid = case config["adapter"]
+ when /^mysql/ then
+ $progress.print "Restoring MySQL database #{config['database']} ... "
+ # Workaround warnings from MySQL 5.6 about passwords on cmd line
+ ENV['MYSQL_PWD'] = config["password"].to_s if config["password"]
+ spawn('mysql', *mysql_args, config['database'], in: decompress_rd)
+ when "postgresql" then
+ $progress.print "Restoring PostgreSQL database #{config['database']} ... "
+ pg_env
+ spawn('psql', config['database'], in: decompress_rd)
+ end
+ decompress_rd.close
+
+ success = [decompress_pid, restore_pid].all? { |pid| Process.waitpid(pid); $?.success? }
+ abort 'Restore failed' unless success
+ end
+
+ protected
+
+ def db_file_name
+ File.join(Gitlab.config.backup.path, 'db', 'database.sql.gz')
+ end
+
+ def mysql_args
+ args = {
+ 'host' => '--host',
+ 'port' => '--port',
+ 'socket' => '--socket',
+ 'username' => '--user',
+ 'encoding' => '--default-character-set'
+ }
+ args.map { |opt, arg| "#{arg}=#{config[opt]}" if config[opt] }.compact
+ end
+
+ def pg_env
+ ENV['PGUSER'] = config["username"] if config["username"]
+ ENV['PGHOST'] = config["host"] if config["host"]
+ ENV['PGPORT'] = config["port"].to_s if config["port"]
+ ENV['PGPASSWORD'] = config["password"].to_s if config["password"]
+ end
+
+ def report_success(success)
+ if success
+ puts '[DONE]'.green
+ else
+ puts '[FAILED]'.red
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/migrate/manager.rb b/lib/ci/migrate/manager.rb
new file mode 100644
index 00000000000..e5e4fb784eb
--- /dev/null
+++ b/lib/ci/migrate/manager.rb
@@ -0,0 +1,72 @@
+module Ci
+ module Migrate
+ class Manager
+ CI_IMPORT_PREFIX = '8.0' # Only allow imports from CI 8.0.x
+
+ def cleanup
+ $progress.print "Deleting tmp directories ... "
+
+ backup_contents.each do |dir|
+ next unless File.exist?(File.join(Gitlab.config.backup.path, dir))
+
+ if FileUtils.rm_rf(File.join(Gitlab.config.backup.path, dir))
+ $progress.puts "done".green
+ else
+ puts "deleting tmp directory '#{dir}' failed".red
+ abort 'Backup failed'
+ end
+ end
+ end
+
+ def unpack
+ Dir.chdir(Gitlab.config.backup.path)
+
+ # check for existing backups in the backup dir
+ file_list = Dir.glob("*_gitlab_ci_backup.tar").each.map { |f| f.split(/_/).first.to_i }
+ puts "no backups found" if file_list.count == 0
+
+ if file_list.count > 1 && ENV["BACKUP"].nil?
+ puts "Found more than one backup, please specify which one you want to restore:"
+ puts "rake gitlab:backup:restore BACKUP=timestamp_of_backup"
+ exit 1
+ end
+
+ tar_file = ENV["BACKUP"].nil? ? File.join("#{file_list.first}_gitlab_ci_backup.tar") : File.join(ENV["BACKUP"] + "_gitlab_ci_backup.tar")
+
+ unless File.exists?(tar_file)
+ puts "The specified CI backup doesn't exist!"
+ exit 1
+ end
+
+ $progress.print "Unpacking backup ... "
+
+ unless Kernel.system(*%W(tar -xf #{tar_file}))
+ puts "unpacking backup failed".red
+ exit 1
+ else
+ $progress.puts "done".green
+ end
+
+ ENV["VERSION"] = "#{settings[:db_version]}" if settings[:db_version].to_i > 0
+
+ # restoring mismatching backups can lead to unexpected problems
+ if !settings[:gitlab_version].start_with?(CI_IMPORT_PREFIX)
+ puts "GitLab CI version mismatch:".red
+ puts " Your current GitLab CI version (#{GitlabCi::VERSION}) differs from the GitLab CI (#{settings[:gitlab_version]}) version in the backup!".red
+ exit 1
+ end
+ end
+
+ private
+
+ def backup_contents
+ ["db", "builds", "backup_information.yml"]
+ end
+
+ def settings
+ @settings ||= YAML.load_file("backup_information.yml")
+ end
+ end
+ end
+end
+
diff --git a/lib/ci/migrate/tags.rb b/lib/ci/migrate/tags.rb
new file mode 100644
index 00000000000..97e043ece27
--- /dev/null
+++ b/lib/ci/migrate/tags.rb
@@ -0,0 +1,42 @@
+require 'yaml'
+
+module Ci
+ module Migrate
+ class Tags
+ def restore
+ puts 'Inserting tags...'
+ connection.select_all('SELECT ci_tags.name FROM ci_tags').each do |tag|
+ begin
+ connection.execute("INSERT INTO tags (name) VALUES(#{ActiveRecord::Base::sanitize(tag['name'])})")
+ rescue ActiveRecord::RecordNotUnique
+ end
+ end
+
+ ActiveRecord::Base.transaction do
+ puts 'Deleting old taggings...'
+ connection.execute "DELETE FROM taggings WHERE context = 'tags' AND taggable_type LIKE 'Ci::%'"
+
+ puts 'Inserting taggings...'
+ connection.execute(
+ 'INSERT INTO taggings (taggable_type, taggable_id, tag_id, context) ' +
+ "SELECT CONCAT('Ci::', ci_taggings.taggable_type), ci_taggings.taggable_id, tags.id, 'tags' FROM ci_taggings " +
+ 'JOIN ci_tags ON ci_tags.id = ci_taggings.tag_id ' +
+ 'JOIN tags ON tags.name = ci_tags.name '
+ )
+
+ puts 'Resetting counters... '
+ connection.execute(
+ 'UPDATE tags SET ' +
+ 'taggings_count = (SELECT COUNT(*) FROM taggings WHERE tags.id = taggings.tag_id)'
+ )
+ end
+ end
+
+ protected
+
+ def connection
+ ActiveRecord::Base.connection
+ end
+ end
+ end
+end
diff --git a/lib/ci/model.rb b/lib/ci/model.rb
new file mode 100644
index 00000000000..c42a0ad36db
--- /dev/null
+++ b/lib/ci/model.rb
@@ -0,0 +1,11 @@
+module Ci
+ module Model
+ def table_name_prefix
+ "ci_"
+ end
+
+ def model_name
+ @model_name ||= ActiveModel::Name.new(self, nil, self.name.split("::").last)
+ end
+ end
+end
diff --git a/lib/ci/scheduler.rb b/lib/ci/scheduler.rb
new file mode 100644
index 00000000000..ee0958f4be1
--- /dev/null
+++ b/lib/ci/scheduler.rb
@@ -0,0 +1,16 @@
+module Ci
+ class Scheduler
+ def perform
+ projects = Ci::Project.where(always_build: true).all
+ projects.each do |project|
+ last_commit = project.commits.last
+ next unless last_commit && last_commit.last_build
+
+ interval = project.polling_interval
+ if (last_commit.last_build.created_at + interval.hours) < Time.now
+ last_commit.retry
+ end
+ end
+ end
+ end
+end
diff --git a/lib/ci/static_model.rb b/lib/ci/static_model.rb
new file mode 100644
index 00000000000..bb2bdbed495
--- /dev/null
+++ b/lib/ci/static_model.rb
@@ -0,0 +1,49 @@
+# Provides an ActiveRecord-like interface to a model whose data is not persisted to a database.
+module Ci
+ module StaticModel
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Used by ActiveRecord's polymorphic association to set object_id
+ def primary_key
+ 'id'
+ end
+
+ # Used by ActiveRecord's polymorphic association to set object_type
+ def base_class
+ self
+ end
+ end
+
+ # Used by AR for fetching attributes
+ #
+ # Pass it along if we respond to it.
+ def [](key)
+ send(key) if respond_to?(key)
+ end
+
+ def to_param
+ id
+ end
+
+ def new_record?
+ false
+ end
+
+ def persisted?
+ false
+ end
+
+ def destroyed?
+ false
+ end
+
+ def ==(other)
+ if other.is_a? ::Ci::StaticModel
+ id == other.id
+ else
+ super
+ end
+ end
+ end
+end
diff --git a/lib/ci/version_info.rb b/lib/ci/version_info.rb
new file mode 100644
index 00000000000..2a87c91db5e
--- /dev/null
+++ b/lib/ci/version_info.rb
@@ -0,0 +1,52 @@
+class VersionInfo
+ include Comparable
+
+ attr_reader :major, :minor, :patch
+
+ def self.parse(str)
+ if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/)
+ VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i)
+ else
+ VersionInfo.new
+ end
+ end
+
+ def initialize(major = 0, minor = 0, patch = 0)
+ @major = major
+ @minor = minor
+ @patch = patch
+ end
+
+ def <=>(other)
+ return unless other.is_a? VersionInfo
+ return unless valid? && other.valid?
+
+ if other.major < @major
+ 1
+ elsif @major < other.major
+ -1
+ elsif other.minor < @minor
+ 1
+ elsif @minor < other.minor
+ -1
+ elsif other.patch < @patch
+ 1
+ elsif @patch < other.patch
+ -1
+ else
+ 0
+ end
+ end
+
+ def to_s
+ if valid?
+ "%d.%d.%d" % [@major, @minor, @patch]
+ else
+ "Unknown"
+ end
+ end
+
+ def valid?
+ @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0
+ end
+end
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index 163937c02cf..f15b2cfd231 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -47,7 +47,7 @@ class EventFilter
actions << Event::COMMENTED if filter.include? 'comments'
- events = events.where(action: actions)
+ events.where(action: actions)
end
def options(key)
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index dc87aa52a3e..0353b3b7ed3 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -1,6 +1,14 @@
require_relative 'shell_env'
module Grack
+ class AuthSpawner
+ def self.call(env)
+ # Avoid issues with instance variables in Grack::Auth persisting across
+ # requests by creating a new instance for each request.
+ Auth.new({}).call(env)
+ end
+ end
+
class Auth < Rack::Auth::Basic
attr_accessor :user, :project, :env
@@ -10,7 +18,7 @@ module Grack
@request = Rack::Request.new(env)
@auth = Request.new(env)
- @gitlab_ci = false
+ @ci = false
# Need this patch due to the rails mount
# Need this if under RELATIVE_URL_ROOT
@@ -28,7 +36,7 @@ module Grack
if project && authorized_request?
# Tell gitlab-git-http-server the request is OK, and what the GL_ID is
render_grack_auth_ok
- elsif @user.nil? && !@gitlab_ci
+ elsif @user.nil? && !@ci
unauthorized
else
render_not_found
@@ -47,8 +55,8 @@ module Grack
# Allow authentication for GitLab CI service
# if valid token passed
- if gitlab_ci_request?(login, password)
- @gitlab_ci = true
+ if ci_request?(login, password)
+ @ci = true
return
end
@@ -60,12 +68,17 @@ module Grack
end
end
- def gitlab_ci_request?(login, password)
- if login == "gitlab-ci-token" && project && project.gitlab_ci?
- token = project.gitlab_ci_service.token
+ def ci_request?(login, password)
+ matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
+
+ if project && matched_login.present? && git_cmd == 'git-upload-pack'
+ underscored_service = matched_login['s'].underscore
+
+ if Service.available_services_names.include?(underscored_service)
+ service_method = "#{underscored_service}_service"
+ service = project.send(service_method)
- if token.present? && token == password && git_cmd == 'git-upload-pack'
- return true
+ return service && service.activated? && service.valid_token?(password)
end
end
@@ -124,7 +137,7 @@ module Grack
end
def authorized_request?
- return true if @gitlab_ci
+ return true if @ci
case git_cmd
when *Gitlab::GitAccess::DOWNLOAD_COMMANDS
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 45bb904ed7a..8a7f8dc5003 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -12,7 +12,6 @@ module Gitlab
@timestamps = {}
date_from = 1.year.ago
- date_to = Date.today
events = Event.reorder(nil).contributions.where(author_id: user.id).
where("created_at > ?", date_from).where(project_id: projects).
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 7ad3ed8728f..0ea1b6a2f6f 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -4,7 +4,7 @@ module Gitlab
key = :current_application_settings
RequestStore.store[key] ||= begin
- if ActiveRecord::Base.connection.active? && ActiveRecord::Base.connection.table_exists?('application_settings')
+ if connect_to_db?
ApplicationSetting.current || ApplicationSetting.create_from_defaults
else
fake_application_settings
@@ -26,5 +26,17 @@ module Gitlab
import_sources: Settings.gitlab['import_sources']
)
end
+
+ private
+
+ def connect_to_db?
+ use_db = if ENV['USE_DB'] == "false"
+ false
+ else
+ true
+ end
+
+ use_db && ActiveRecord::Base.connection.active? && ActiveRecord::Base.connection.table_exists?('application_settings')
+ end
end
end
diff --git a/lib/gitlab/diff/file.rb b/lib/gitlab/diff/file.rb
index 4daf65331e8..142058aa69d 100644
--- a/lib/gitlab/diff/file.rb
+++ b/lib/gitlab/diff/file.rb
@@ -44,6 +44,14 @@ module Gitlab
diff.old_path
end
end
+
+ def added_lines
+ diff_lines.select(&:added?).size
+ end
+
+ def removed_lines
+ diff_lines.select(&:removed?).size
+ end
end
end
end
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 8ac1b15e88a..0072194606e 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -7,6 +7,14 @@ module Gitlab
@text, @type, @index = text, type, index
@old_pos, @new_pos = old_pos, new_pos
end
+
+ def added?
+ type == 'new'
+ end
+
+ def removed?
+ type == 'old'
+ end
end
end
end
diff --git a/lib/gitlab/diff/parser.rb b/lib/gitlab/diff/parser.rb
index c1d9520ddf1..7015fe36c3d 100644
--- a/lib/gitlab/diff/parser.rb
+++ b/lib/gitlab/diff/parser.rb
@@ -14,8 +14,6 @@ module Gitlab
lines_arr = ::Gitlab::InlineDiff.processing lines
lines_arr.each do |line|
- raw_line = line.dup
-
next if filename?(line)
full_line = html_escape(line.gsub(/\n/, ''))
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 355fbd27898..2b252b32887 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -65,7 +65,7 @@ module Gitlab
def reply_key
reply_key = nil
message.to.each do |address|
- reply_key = Gitlab::ReplyByEmail.reply_key_from_address(address)
+ reply_key = Gitlab::IncomingEmail.key_from_address(address)
break if reply_key
end
@@ -98,7 +98,8 @@ module Gitlab
note: reply,
noteable_type: sent_notification.noteable_type,
noteable_id: sent_notification.noteable_id,
- commit_id: sent_notification.commit_id
+ commit_id: sent_notification.commit_id,
+ line_code: sent_notification.line_code
).execute
end
end
diff --git a/lib/gitlab/fogbugz_import/client.rb b/lib/gitlab/fogbugz_import/client.rb
new file mode 100644
index 00000000000..431d50882fd
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/client.rb
@@ -0,0 +1,56 @@
+require 'fogbugz'
+
+module Gitlab
+ module FogbugzImport
+ class Client
+ attr_reader :api
+
+ def initialize(options = {})
+ if options[:uri] && options[:token]
+ @api = ::Fogbugz::Interface.new(options)
+ elsif options[:uri] && options[:email] && options[:password]
+ @api = ::Fogbugz::Interface.new(options)
+ @api.authenticate
+ @api
+ end
+ end
+
+ def get_token
+ @api.token
+ end
+
+ def valid?
+ !get_token.blank?
+ end
+
+ def user_map
+ users = {}
+ res = @api.command(:listPeople)
+ res['people']['person'].each do |user|
+ users[user['ixPerson']] = { name: user['sFullName'], email: user['sEmail'] }
+ end
+ users
+ end
+
+ def repos
+ res = @api.command(:listProjects)
+ @repos ||= res['projects']['project'].map { |proj| FogbugzImport::Repository.new(proj) }
+ end
+
+ def repo(id)
+ repos.find { |r| r.id.to_s == id.to_s }
+ end
+
+ def cases(project_id)
+ project_name = repo(project_id).name
+ res = @api.command(:search, q: "project:'#{project_name}'", cols: 'ixPersonAssignedTo,ixPersonOpenedBy,ixPersonClosedBy,sStatus,sPriority,sCategory,fOpen,sTitle,sLatestTextSummary,dtOpened,dtClosed,dtResolved,dtLastUpdated,events')
+ return [] unless res['cases']['count'].to_i > 0
+ res['cases']['case']
+ end
+
+ def categories
+ @api.command(:listCategories)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
new file mode 100644
index 00000000000..496256700b8
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -0,0 +1,298 @@
+module Gitlab
+ module FogbugzImport
+ class Importer
+ attr_reader :project, :repo
+
+ def initialize(project)
+ @project = project
+
+ import_data = project.import_data.try(:data)
+ repo_data = import_data['repo'] if import_data
+ @repo = FogbugzImport::Repository.new(repo_data)
+
+ @known_labels = Set.new
+ end
+
+ def execute
+ return true unless repo.valid?
+
+ data = project.import_data.try(:data)
+
+ client = Gitlab::FogbugzImport::Client.new(token: data['fb_session']['token'], uri: data['fb_session']['uri'])
+
+ @cases = client.cases(@repo.id.to_i)
+ @categories = client.categories
+
+ import_cases
+
+ true
+ end
+
+ private
+
+ def user_map
+ @user_map ||= begin
+ user_map = Hash.new
+ import_data = project.import_data.try(:data)
+ stored_user_map = import_data['user_map'] if import_data
+ user_map.update(stored_user_map) if stored_user_map
+
+ user_map
+ end
+ end
+
+ def import_labels
+ @categories['categories']['category'].each do |label|
+ create_label(label['sCategory'])
+ @known_labels << name
+ end
+ end
+
+ def nice_label_color(name)
+ case name
+ when 'Blocker'
+ '#ff0000'
+ when 'Crash'
+ '#ffcfcf'
+ when 'Major'
+ '#deffcf'
+ when 'Minor'
+ '#cfe9ff'
+ when 'Bug'
+ '#d9534f'
+ when 'Feature'
+ '#44ad8e'
+ when 'Technical Task'
+ '#4b6dd0'
+ else
+ '#e2e2e2'
+ end
+ end
+
+ def create_label(name)
+ color = nice_label_color(name)
+ Label.create!(project_id: project.id, title: name, color: color)
+ end
+
+ def user_info(person_id)
+ user_hash = user_map[person_id.to_s]
+
+ user_name = ''
+ gitlab_id = nil
+
+ unless user_hash.nil?
+ user_name = user_hash['name']
+ if user = User.find_by(id: user_hash['gitlab_user'])
+ user_name = "@#{user.username}"
+ gitlab_id = user.id
+ end
+ end
+
+ { name: user_name, gitlab_id: gitlab_id }
+ end
+
+ def import_cases
+ return unless @cases
+
+ while bug = @cases.shift
+ author = user_info(bug['ixPersonOpenedBy'])[:name]
+ date = DateTime.parse(bug['dtOpened'])
+
+ comments = bug['events']['event']
+
+ content = format_content(opened_content(comments))
+ body = format_issue_body(author, date, content)
+
+ labels = []
+ [bug['sCategory'], bug['sPriority']].each do |label|
+ unless label.blank?
+ labels << label
+ unless @known_labels.include?(label)
+ create_label(label)
+ @known_labels << label
+ end
+ end
+ end
+
+ assignee_id = user_info(bug['ixPersonAssignedTo'])[:gitlab_id]
+ author_id = user_info(bug['ixPersonOpenedBy'])[:gitlab_id] || project.creator_id
+
+ issue = Issue.create!(
+ project_id: project.id,
+ title: bug['sTitle'],
+ description: body,
+ author_id: author_id,
+ assignee_id: assignee_id,
+ state: bug['fOpen'] == 'true' ? 'opened' : 'closed'
+ )
+ issue.add_labels_by_names(labels)
+
+ if issue.iid != bug['ixBug']
+ issue.update_attribute(:iid, bug['ixBug'])
+ end
+
+ import_issue_comments(issue, comments)
+
+ issue.update_attribute(:created_at, date)
+
+ last_update = DateTime.parse(bug['dtLastUpdated'])
+ issue.update_attribute(:updated_at, last_update)
+ end
+ end
+
+ def opened_content(comments)
+ while comment = comments.shift
+ if comment['sVerb'] == 'Opened'
+ return comment['s']
+ end
+ end
+ ''
+ end
+
+ def import_issue_comments(issue, comments)
+ Note.transaction do
+ while comment = comments.shift
+ verb = comment['sVerb']
+
+ next if verb == 'Opened'
+
+ content = format_content(comment['s'])
+ attachments = format_attachments(comment['rgAttachments'])
+ updates = format_updates(comment)
+
+ next if content.blank? && attachments.empty? && updates.empty?
+
+ author = user_info(comment['ixPerson'])[:name]
+ author_id = user_info(comment['ixPerson'])[:gitlab_id] || project.creator_id
+ date = DateTime.parse(comment['dt'])
+
+ body = format_issue_comment_body(
+ comment['ixBugEvent'],
+ author,
+ date,
+ content,
+ attachments,
+ updates
+ )
+
+ note = Note.create!(
+ project_id: project.id,
+ noteable_type: "Issue",
+ noteable_id: issue.id,
+ author_id: author_id,
+ note: body
+ )
+
+ note.update_attribute(:created_at, date)
+ note.update_attribute(:updated_at, date)
+ end
+ end
+ end
+
+ def linkify_issues(s)
+ s = s.gsub(/([Ii]ssue) ([0-9]+)/, '\1 #\2')
+ s = s.gsub(/([Cc]ase) ([0-9]+)/, '\1 #\2')
+ s
+ end
+
+ def escape_for_markdown(s)
+ s = s.gsub(/^#/, "\\#")
+ s = s.gsub(/^-/, "\\-")
+ s = s.gsub("`", "\\~")
+ s = s.gsub("\r", "")
+ s = s.gsub("\n", " \n")
+ s
+ end
+
+ def format_content(raw_content)
+ return raw_content if raw_content.nil?
+ linkify_issues(escape_for_markdown(raw_content))
+ end
+
+ def format_attachments(raw_attachments)
+ return [] unless raw_attachments
+
+ attachments = case raw_attachments['attachment']
+ when Array
+ raw_attachments['attachment']
+ when Hash
+ [raw_attachments['attachment']]
+ else
+ []
+ end
+
+ attachments.map! { |a| format_attachment(a) }
+ attachments.compact
+ end
+
+ def format_attachment(attachment)
+ link = build_attachment_url(attachment['sURL'])
+
+ res = ::Projects::DownloadService.new(project, link).execute
+
+ return nil if res.nil?
+
+ text = "[#{res['alt']}](#{res['url']})"
+ text = "!#{text}" if res['is_image']
+ text
+ end
+
+ def build_attachment_url(rel_url)
+ data = project.import_data.try(:data)
+ uri = data['fb_session']['uri']
+ token = data['fb_session']['token']
+ "#{uri}/#{rel_url}&token=#{token}"
+ end
+
+ def format_updates(comment)
+ updates = []
+
+ if comment['sChanges']
+ updates << "*Changes: #{linkify_issues(comment['sChanges'].chomp)}*"
+ end
+
+ if comment['evtDescription']
+ updates << "*#{comment['evtDescription']}*"
+ end
+
+ updates
+ end
+
+ def format_issue_body(author, date, content)
+ body = []
+ body << "*By #{author} on #{date} (imported from FogBugz)*"
+ body << '---'
+
+ if content.blank?
+ content = '*(No description has been entered for this issue)*'
+ end
+ body << content
+
+ body.join("\n\n")
+ end
+
+ def format_issue_comment_body(id, author, date, content, attachments, updates)
+ body = []
+ body << "*By #{author} on #{date} (imported from FogBugz)*"
+ body << '---'
+
+ if content.blank?
+ content = "*(No comment has been entered for this change)*"
+ end
+ body << content
+
+ if updates.any?
+ body << '---'
+ body += updates
+ end
+
+ if attachments.any?
+ body << '---'
+ body += attachments
+ end
+
+ body.join("\n\n")
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/project_creator.rb b/lib/gitlab/fogbugz_import/project_creator.rb
new file mode 100644
index 00000000000..8b1b6f48ed5
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/project_creator.rb
@@ -0,0 +1,38 @@
+module Gitlab
+ module FogbugzImport
+ class ProjectCreator
+ attr_reader :repo, :fb_session, :namespace, :current_user, :user_map
+
+ def initialize(repo, fb_session, namespace, current_user, user_map = nil)
+ @repo = repo
+ @fb_session = fb_session
+ @namespace = namespace
+ @current_user = current_user
+ @user_map = user_map
+ end
+
+ def execute
+ project = ::Projects::CreateService.new(current_user,
+ name: repo.safe_name,
+ path: repo.path,
+ namespace: namespace,
+ creator: current_user,
+ visibility_level: Gitlab::VisibilityLevel::INTERNAL,
+ import_type: 'fogbugz',
+ import_source: repo.name,
+ import_url: Project::UNKNOWN_IMPORT_URL
+ ).execute
+
+ project.create_import_data(
+ data: {
+ 'repo' => repo.raw_data,
+ 'user_map' => user_map,
+ 'fb_session' => fb_session
+ }
+ )
+
+ project
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/fogbugz_import/repository.rb b/lib/gitlab/fogbugz_import/repository.rb
new file mode 100644
index 00000000000..d1dc63db2b2
--- /dev/null
+++ b/lib/gitlab/fogbugz_import/repository.rb
@@ -0,0 +1,31 @@
+module Gitlab
+ module FogbugzImport
+ class Repository
+ attr_accessor :raw_data
+
+ def initialize(raw_data)
+ @raw_data = raw_data
+ end
+
+ def valid?
+ raw_data.is_a?(Hash)
+ end
+
+ def id
+ raw_data['ixProject']
+ end
+
+ def name
+ raw_data['sProject']
+ end
+
+ def safe_name
+ name.gsub(/[^\s\w.-]/, '')
+ end
+
+ def path
+ safe_name.gsub(/[\s]/, '_')
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/google_code_import/project_creator.rb b/lib/gitlab/google_code_import/project_creator.rb
index 0cfeaf9d61c..1cb7d16aeb3 100644
--- a/lib/gitlab/google_code_import/project_creator.rb
+++ b/lib/gitlab/google_code_import/project_creator.rb
@@ -23,7 +23,7 @@ module Gitlab
import_url: repo.import_url
).execute
- import_data = project.create_import_data(
+ project.create_import_data(
data: {
"repo" => repo.raw_data,
"user_map" => user_map
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 991b70aab6a..ccfdfbe73e8 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -19,6 +19,7 @@ module Gitlab
'GitLab.com' => 'gitlab',
'Gitorious.org' => 'gitorious',
'Google Code' => 'google_code',
+ 'FogBugz' => 'fogbugz',
'Any repo by URL' => 'git',
}
end
diff --git a/lib/gitlab/reply_by_email.rb b/lib/gitlab/incoming_email.rb
index c3fe6778f06..856ccc71084 100644
--- a/lib/gitlab/reply_by_email.rb
+++ b/lib/gitlab/incoming_email.rb
@@ -1,5 +1,5 @@
module Gitlab
- module ReplyByEmail
+ module IncomingEmail
class << self
def enabled?
config.enabled && address_formatted_correctly?
@@ -7,20 +7,14 @@ module Gitlab
def address_formatted_correctly?
config.address &&
- config.address.include?("%{reply_key}")
+ config.address.include?("%{key}")
end
- def reply_key
- return nil unless enabled?
-
- SecureRandom.hex(16)
- end
-
- def reply_address(reply_key)
- config.address.gsub('%{reply_key}', reply_key)
+ def reply_address(key)
+ config.address.gsub('%{key}', key)
end
- def reply_key_from_address(address)
+ def key_from_address(address)
regex = address_regex
return unless regex
@@ -33,7 +27,7 @@ module Gitlab
private
def config
- Gitlab.config.reply_by_email
+ Gitlab.config.incoming_email
end
def address_regex
@@ -41,7 +35,7 @@ module Gitlab
return nil unless wildcard_address
regex = Regexp.escape(wildcard_address)
- regex = regex.gsub(Regexp.escape('%{reply_key}'), "(.+)")
+ regex = regex.gsub(Regexp.escape('%{key}'), "(.+)")
Regexp.new(regex).freeze
end
end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
new file mode 100644
index 00000000000..bf4dd9542d5
--- /dev/null
+++ b/lib/gitlab/ldap/auth_hash.rb
@@ -0,0 +1,36 @@
+# Class to parse and transform the info provided by omniauth
+#
+module Gitlab
+ module LDAP
+ class AuthHash < Gitlab::OAuth::AuthHash
+ private
+
+ def get_info(key)
+ attributes = ldap_config.attributes[key.to_s]
+ return super unless attributes
+
+ attributes = Array(attributes)
+
+ value = nil
+ attributes.each do |attribute|
+ value = get_raw(attribute)
+ value = value.first if value
+ break if value.present?
+ end
+
+ return super unless value
+
+ Gitlab::Utils.force_utf8(value)
+ value
+ end
+
+ def get_raw(key)
+ auth_hash.extra[:raw_info][key]
+ end
+
+ def ldap_config
+ @ldap_config ||= Gitlab::LDAP::Config.new(self.provider)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
index d2ffa2e1fe8..101a3285f4b 100644
--- a/lib/gitlab/ldap/config.rb
+++ b/lib/gitlab/ldap/config.rb
@@ -84,6 +84,10 @@ module Gitlab
options['block_auto_created_users']
end
+ def attributes
+ options['attributes']
+ end
+
protected
def base_config
Gitlab.config.ldap
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
index 04a22237478..1ea7751e27d 100644
--- a/lib/gitlab/ldap/user.rb
+++ b/lib/gitlab/ldap/user.rb
@@ -14,7 +14,7 @@ module Gitlab
# LDAP distinguished name is case-insensitive
identity = ::Identity.
where(provider: provider).
- where('lower(extern_uid) = ?', uid.downcase).last
+ where('lower(extern_uid) = ?', uid.mb_chars.downcase.to_s).last
identity && identity.user
end
end
@@ -71,6 +71,10 @@ module Gitlab
def ldap_config
Gitlab::LDAP::Config.new(auth_hash.provider)
end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = Gitlab::LDAP::AuthHash.new(auth_hash)
+ end
end
end
end
diff --git a/lib/gitlab/markdown.rb b/lib/gitlab/markdown.rb
index 476c736a11a..f389d0a80dd 100644
--- a/lib/gitlab/markdown.rb
+++ b/lib/gitlab/markdown.rb
@@ -103,7 +103,7 @@ module Gitlab
# EmojiFilter
asset_host: Gitlab::Application.config.asset_host,
- asset_root: Gitlab.config.gitlab.url,
+ asset_root: Gitlab.config.gitlab.base_url,
# ReferenceFilter
only_path: only_path_pipeline?(options[:pipeline]),
diff --git a/lib/gitlab/markdown/commit_range_reference_filter.rb b/lib/gitlab/markdown/commit_range_reference_filter.rb
index 8613150894b..bb496135d92 100644
--- a/lib/gitlab/markdown/commit_range_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_range_reference_filter.rb
@@ -73,7 +73,7 @@ module Gitlab
end
def url_for_commit_range(project, range)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_compare_url(project.namespace, project,
range.to_param.merge(only_path: context[:only_path]))
end
diff --git a/lib/gitlab/markdown/commit_reference_filter.rb b/lib/gitlab/markdown/commit_reference_filter.rb
index 5696b4fa585..fcbb2e936a5 100644
--- a/lib/gitlab/markdown/commit_reference_filter.rb
+++ b/lib/gitlab/markdown/commit_reference_filter.rb
@@ -69,7 +69,7 @@ module Gitlab
end
def url_for_commit(project, commit)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_commit_url(project.namespace, project, commit,
only_path: context[:only_path])
end
diff --git a/lib/gitlab/markdown/label_reference_filter.rb b/lib/gitlab/markdown/label_reference_filter.rb
index 3d7445a27f1..1e5cb12071e 100644
--- a/lib/gitlab/markdown/label_reference_filter.rb
+++ b/lib/gitlab/markdown/label_reference_filter.rb
@@ -56,7 +56,7 @@ module Gitlab
end
def url_for_label(project, label)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_issues_path(project.namespace, project,
label_name: label.name,
only_path: context[:only_path])
diff --git a/lib/gitlab/markdown/merge_request_reference_filter.rb b/lib/gitlab/markdown/merge_request_reference_filter.rb
index 48248f5219d..ecbd263d0e0 100644
--- a/lib/gitlab/markdown/merge_request_reference_filter.rb
+++ b/lib/gitlab/markdown/merge_request_reference_filter.rb
@@ -63,7 +63,7 @@ module Gitlab
end
def url_for_merge_request(mr, project)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_merge_request_url(project.namespace, project, mr,
only_path: context[:only_path])
end
diff --git a/lib/gitlab/markdown/relative_link_filter.rb b/lib/gitlab/markdown/relative_link_filter.rb
index 8c5cf51bfe1..6ee3d1ce039 100644
--- a/lib/gitlab/markdown/relative_link_filter.rb
+++ b/lib/gitlab/markdown/relative_link_filter.rb
@@ -59,25 +59,43 @@ module Gitlab
end
def relative_file_path(path)
- nested_path = build_nested_path(path, context[:requested_path])
+ nested_path = build_relative_path(path, context[:requested_path])
file_exists?(nested_path) ? nested_path : path
end
- # Covering a special case, when the link is referencing file in the same
- # directory.
- # If we are at doc/api/README.md and the README.md contains relative
- # links like [Users](users.md), this takes the request
- # path(doc/api/README.md) and replaces the README.md with users.md so the
- # path looks like doc/api/users.md.
- # If we are at doc/api and the README.md shown in below the tree view
- # this takes the request path(doc/api) and adds users.md so the path
- # looks like doc/api/users.md
- def build_nested_path(path, request_path)
+ # Convert a relative path into its correct location based on the currently
+ # requested path
+ #
+ # path - Relative path String
+ # request_path - Currently-requested path String
+ #
+ # Examples:
+ #
+ # # File in the same directory as the current path
+ # build_relative_path("users.md", "doc/api/README.md")
+ # # => "doc/api/users.md"
+ #
+ # # File in the same directory, which is also the current path
+ # build_relative_path("users.md", "doc/api")
+ # # => "doc/api/users.md"
+ #
+ # # Going up one level to a different directory
+ # build_relative_path("../update/7.14-to-8.0.md", "doc/api/README.md")
+ # # => "doc/update/7.14-to-8.0.md"
+ #
+ # Returns a String
+ def build_relative_path(path, request_path)
return request_path if path.empty?
return path unless request_path
parts = request_path.split('/')
parts.pop if path_type(request_path) != 'tree'
+
+ while parts.length > 1 && path.start_with?('../')
+ parts.pop
+ path.sub!('../', '')
+ end
+
parts.push(path).join('/')
end
diff --git a/lib/gitlab/markdown/sanitization_filter.rb b/lib/gitlab/markdown/sanitization_filter.rb
index 68ed57f6257..e368de7d848 100644
--- a/lib/gitlab/markdown/sanitization_filter.rb
+++ b/lib/gitlab/markdown/sanitization_filter.rb
@@ -67,12 +67,16 @@ module Gitlab
def clean_spans
lambda do |env|
- return unless env[:node_name] == 'span'
- return unless env[:node].has_attribute?('class')
+ node = env[:node]
- unless has_ancestor?(env[:node], 'pre')
- env[:node].remove_attribute('class')
+ return unless node.name == 'span'
+ return unless node.has_attribute?('class')
+
+ unless has_ancestor?(node, 'pre')
+ node.remove_attribute('class')
end
+
+ { node_whitelist: [node] }
end
end
end
diff --git a/lib/gitlab/markdown/snippet_reference_filter.rb b/lib/gitlab/markdown/snippet_reference_filter.rb
index 9e1aab936cb..e2cf89cb1d8 100644
--- a/lib/gitlab/markdown/snippet_reference_filter.rb
+++ b/lib/gitlab/markdown/snippet_reference_filter.rb
@@ -63,7 +63,7 @@ module Gitlab
end
def url_for_snippet(snippet, project)
- h = Rails.application.routes.url_helpers
+ h = Gitlab::Application.routes.url_helpers
h.namespace_project_snippet_url(project.namespace, project, snippet,
only_path: context[:only_path])
end
diff --git a/lib/gitlab/markdown/syntax_highlight_filter.rb b/lib/gitlab/markdown/syntax_highlight_filter.rb
index 86f4385753a..8597e02f0de 100644
--- a/lib/gitlab/markdown/syntax_highlight_filter.rb
+++ b/lib/gitlab/markdown/syntax_highlight_filter.rb
@@ -21,7 +21,13 @@ module Gitlab
language = node.attr('class')
code = node.text
- highlighted = block_code(code, language)
+ begin
+ highlighted = block_code(code, language)
+ rescue
+ # Gracefully handle syntax highlighter bugs/errors to ensure
+ # users can still access an issue/comment/etc.
+ highlighted = "<pre>#{code}</pre>"
+ end
# Replace the parent `pre` element with the entire highlighted block
node.parent.replace(highlighted)
diff --git a/lib/gitlab/markdown/user_reference_filter.rb b/lib/gitlab/markdown/user_reference_filter.rb
index 0446cd49f8e..c08811effb2 100644
--- a/lib/gitlab/markdown/user_reference_filter.rb
+++ b/lib/gitlab/markdown/user_reference_filter.rb
@@ -51,7 +51,7 @@ module Gitlab
private
def urls
- Rails.application.routes.url_helpers
+ Gitlab::Application.routes.url_helpers
end
def link_class
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
index 9b8e783d16c..d94b104bbf8 100644
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ b/lib/gitlab/o_auth/auth_hash.rb
@@ -16,16 +16,6 @@ module Gitlab
@provider ||= Gitlab::Utils.force_utf8(auth_hash.provider.to_s)
end
- def info
- auth_hash.info
- end
-
- def get_info(key)
- value = info.try(key)
- Gitlab::Utils.force_utf8(value) if value
- value
- end
-
def name
@name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}"
end
@@ -44,9 +34,19 @@ module Gitlab
private
+ def info
+ auth_hash.info
+ end
+
+ def get_info(key)
+ value = info[key]
+ Gitlab::Utils.force_utf8(value) if value
+ value
+ end
+
def username_and_email
@username_and_email ||= begin
- username = get_info(:nickname) || get_info(:username)
+ username = get_info(:username) || get_info(:nickname)
email = get_info(:email)
username ||= generate_username(email) if email
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index 11b0d44f340..6f0d02cafd1 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -1,6 +1,6 @@
module Gitlab
class UrlBuilder
- include Rails.application.routes.url_helpers
+ include Gitlab::Application.routes.url_helpers
include GitlabRoutingHelper
def initialize(type)
@@ -23,12 +23,12 @@ module Gitlab
def build_issue_url(id)
issue = Issue.find(id)
- issue_url(issue, host: Gitlab.config.gitlab['url'])
+ issue_url(issue)
end
def build_merge_request_url(id)
merge_request = MergeRequest.find(id)
- merge_request_url(merge_request, host: Gitlab.config.gitlab['url'])
+ merge_request_url(merge_request)
end
def build_note_url(id)
@@ -37,22 +37,18 @@ module Gitlab
namespace_project_commit_url(namespace_id: note.project.namespace,
id: note.commit_id,
project_id: note.project,
- host: Gitlab.config.gitlab['url'],
anchor: "note_#{note.id}")
elsif note.for_issue?
issue = Issue.find(note.noteable_id)
issue_url(issue,
- host: Gitlab.config.gitlab['url'],
anchor: "note_#{note.id}")
elsif note.for_merge_request?
merge_request = MergeRequest.find(note.noteable_id)
merge_request_url(merge_request,
- host: Gitlab.config.gitlab['url'],
anchor: "note_#{note.id}")
elsif note.for_project_snippet?
snippet = Snippet.find(note.noteable_id)
project_snippet_url(snippet,
- host: Gitlab.config.gitlab['url'],
anchor: "note_#{note.id}")
end
end
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 17f89c8beb6..7218a4d2f20 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -124,6 +124,15 @@ server {
proxy_connect_timeout 300;
proxy_redirect off;
+ # Do not buffer Git HTTP responses
+ proxy_buffering off;
+
+ # The following settings only work with NGINX 1.7.11 or newer
+ #
+ # # Pass chunked request bodies to gitlab-git-http-server as-is
+ # proxy_request_buffering off;
+ # proxy_http_version 1.1;
+
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index 5ba39fc41a4..7dabfba87e2 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -171,6 +171,15 @@ server {
proxy_connect_timeout 300;
proxy_redirect off;
+ # Do not buffer Git HTTP responses
+ proxy_buffering off;
+
+ # The following settings only work with NGINX 1.7.11 or newer
+ #
+ # # Pass chunked request bodies to gitlab-git-http-server as-is
+ # proxy_request_buffering off;
+ # proxy_http_version 1.1;
+
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-Ssl on;
diff --git a/lib/support/nginx/gitlab_ci b/lib/support/nginx/gitlab_ci
new file mode 100644
index 00000000000..bf05edfd780
--- /dev/null
+++ b/lib/support/nginx/gitlab_ci
@@ -0,0 +1,29 @@
+# GITLAB CI
+server {
+ listen 80 default_server; # e.g., listen 192.168.1.1:80;
+ server_name YOUR_CI_SERVER_FQDN; # e.g., server_name source.example.com;
+
+ access_log /var/log/nginx/gitlab_ci_access.log;
+ error_log /var/log/nginx/gitlab_ci_error.log;
+
+ # expose API to fix runners
+ location /api {
+ proxy_read_timeout 300;
+ proxy_connect_timeout 300;
+ proxy_redirect off;
+ proxy_set_header X-Real-IP $remote_addr;
+
+ # You need to specify your DNS servers that are able to resolve YOUR_GITLAB_SERVER_FQDN
+ resolver 8.8.8.8 8.8.4.4;
+ proxy_pass $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # redirect all other CI requests
+ location / {
+ return 301 $scheme://YOUR_GITLAB_SERVER_FQDN/ci$request_uri;
+ }
+
+ # adjust this to match the largest build log your runners might submit,
+ # set to 0 to disable limit
+ client_max_body_size 10m;
+} \ No newline at end of file
diff --git a/lib/tasks/ci/.gitkeep b/lib/tasks/ci/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/lib/tasks/ci/.gitkeep
diff --git a/lib/tasks/ci/cleanup.rake b/lib/tasks/ci/cleanup.rake
new file mode 100644
index 00000000000..2f4d11bd942
--- /dev/null
+++ b/lib/tasks/ci/cleanup.rake
@@ -0,0 +1,8 @@
+namespace :ci do
+ namespace :cleanup do
+ desc "GitLab CI | Clean running builds"
+ task builds: :environment do
+ Ci::Build.running.update_all(status: 'canceled')
+ end
+ end
+end
diff --git a/lib/tasks/ci/migrate.rake b/lib/tasks/ci/migrate.rake
new file mode 100644
index 00000000000..1de664c85e1
--- /dev/null
+++ b/lib/tasks/ci/migrate.rake
@@ -0,0 +1,87 @@
+namespace :ci do
+ desc 'GitLab | Import and migrate CI database'
+ task migrate: :environment do
+ warn_user_is_not_gitlab
+ configure_cron_mode
+
+ unless ENV['force'] == 'yes'
+ puts 'This will remove all CI related data and restore it from the provided backup.'
+ ask_to_continue
+ puts ''
+ end
+
+ # disable CI for time of migration
+ enable_ci(false)
+
+ # unpack archives
+ migrate = Ci::Migrate::Manager.new
+ migrate.unpack
+
+ Rake::Task['ci:migrate:db'].invoke
+ Rake::Task['ci:migrate:builds'].invoke
+ Rake::Task['ci:migrate:tags'].invoke
+ Rake::Task['ci:migrate:services'].invoke
+
+ # enable CI for time of migration
+ enable_ci(true)
+
+ migrate.cleanup
+ end
+
+ namespace :migrate do
+ desc 'GitLab | Import CI database'
+ task db: :environment do
+ configure_cron_mode
+ $progress.puts 'Restoring database ... '.blue
+ Ci::Migrate::Database.new.restore
+ $progress.puts 'done'.green
+ end
+
+ desc 'GitLab | Import CI builds'
+ task builds: :environment do
+ configure_cron_mode
+ $progress.puts 'Restoring builds ... '.blue
+ Ci::Migrate::Builds.new.restore
+ $progress.puts 'done'.green
+ end
+
+ desc 'GitLab | Migrate CI tags'
+ task tags: :environment do
+ configure_cron_mode
+ $progress.puts 'Migrating tags ... '.blue
+ ::Ci::Migrate::Tags.new.restore
+ $progress.puts 'done'.green
+ end
+
+ desc 'GitLab | Migrate CI auto-increments'
+ task autoincrements: :environment do
+ c = ActiveRecord::Base.connection
+ c.tables.select { |t| t.start_with?('ci_') }.each do |table|
+ result = c.select_one("SELECT id FROM #{table} ORDER BY id DESC LIMIT 1")
+ if result
+ ai_val = result['id'].to_i + 1
+ puts "Resetting auto increment ID for #{table} to #{ai_val}"
+ if c.adapter_name == 'PostgreSQL'
+ c.execute("ALTER SEQUENCE #{table}_id_seq RESTART WITH #{ai_val}")
+ else
+ c.execute("ALTER TABLE #{table} AUTO_INCREMENT = #{ai_val}")
+ end
+ end
+ end
+ end
+
+ desc 'GitLab | Migrate CI services'
+ task services: :environment do
+ $progress.puts 'Migrating services ... '.blue
+ c = ActiveRecord::Base.connection
+ c.execute("UPDATE ci_services SET type=CONCAT('Ci::', type) WHERE type NOT LIKE 'Ci::%'")
+ $progress.puts 'done'.green
+ end
+ end
+
+ def enable_ci(enabled)
+ settings = ApplicationSetting.current || ApplicationSetting.create_from_defaults
+ settings.ci_enabled = enabled
+ settings.save!
+ end
+end
diff --git a/lib/tasks/ci/schedule_builds.rake b/lib/tasks/ci/schedule_builds.rake
new file mode 100644
index 00000000000..49435504c67
--- /dev/null
+++ b/lib/tasks/ci/schedule_builds.rake
@@ -0,0 +1,6 @@
+namespace :ci do
+ desc "GitLab CI | Clean running builds"
+ task schedule_builds: :environment do
+ Ci::Scheduler.new.perform
+ end
+end
diff --git a/lib/tasks/gitlab/backup.rake b/lib/tasks/gitlab/backup.rake
index 4c73f90bbf2..f20c7f71ba5 100644
--- a/lib/tasks/gitlab/backup.rake
+++ b/lib/tasks/gitlab/backup.rake
@@ -11,6 +11,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:db:create"].invoke
Rake::Task["gitlab:backup:repo:create"].invoke
Rake::Task["gitlab:backup:uploads:create"].invoke
+ Rake::Task["gitlab:backup:builds:create"].invoke
backup = Backup::Manager.new
backup.pack
@@ -30,6 +31,7 @@ namespace :gitlab do
Rake::Task["gitlab:backup:db:restore"].invoke unless backup.skipped?("db")
Rake::Task["gitlab:backup:repo:restore"].invoke unless backup.skipped?("repositories")
Rake::Task["gitlab:backup:uploads:restore"].invoke unless backup.skipped?("uploads")
+ Rake::Task["gitlab:backup:builds:restore"].invoke unless backup.skipped?("builds")
Rake::Task["gitlab:shell:setup"].invoke
backup.cleanup
@@ -73,6 +75,25 @@ namespace :gitlab do
end
end
+ namespace :builds do
+ task create: :environment do
+ $progress.puts "Dumping builds ... ".blue
+
+ if ENV["SKIP"] && ENV["SKIP"].include?("builds")
+ $progress.puts "[SKIPPED]".cyan
+ else
+ Backup::Builds.new.dump
+ $progress.puts "done".green
+ end
+ end
+
+ task restore: :environment do
+ $progress.puts "Restoring builds ... ".blue
+ Backup::Builds.new.restore
+ $progress.puts "done".green
+ end
+ end
+
namespace :uploads do
task create: :environment do
$progress.puts "Dumping uploads ... ".blue
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index 2b9688c1b40..66f1ecf385f 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -2,7 +2,7 @@ namespace :gitlab do
desc "GitLab | Check the configuration of GitLab and its environment"
task check: %w{gitlab:gitlab_shell:check
gitlab:sidekiq:check
- gitlab:reply_by_email:check
+ gitlab:incoming_email:check
gitlab:ldap:check
gitlab:app:check}
@@ -278,7 +278,7 @@ namespace :gitlab do
fix_and_rerun
end
end
-
+
def check_uploads
print "Uploads directory setup correctly? ... "
@@ -331,15 +331,18 @@ namespace :gitlab do
end
def check_redis_version
- print "Redis version >= 2.0.0? ... "
+ min_redis_version = "2.4.0"
+ print "Redis version >= #{min_redis_version}? ... "
redis_version = run(%W(redis-cli --version))
- if redis_version.try(:match, /redis-cli 2.\d.\d/) || redis_version.try(:match, /redis-cli 3.\d.\d/)
+ redis_version = redis_version.try(:match, /redis-cli (.*)/)
+ if redis_version &&
+ (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
puts "yes".green
else
puts "no".red
try_fixing_it(
- "Update your redis server to a version >= 2.0.0"
+ "Update your redis server to a version >= #{min_redis_version}"
)
for_more_information(
"gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq"
@@ -488,7 +491,7 @@ namespace :gitlab do
else
puts "wrong or missing hooks".red
try_fixing_it(
- sudo_gitlab("#{gitlab_shell_path}/bin/create-hooks"),
+ sudo_gitlab("#{File.join(gitlab_shell_path, 'bin/create-hooks')}"),
'Check the hooks_path in config/gitlab.yml',
'Check your gitlab-shell installation'
)
@@ -631,13 +634,13 @@ namespace :gitlab do
end
- namespace :reply_by_email do
+ namespace :incoming_email do
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
warn_user_is_not_gitlab
start_checking "Reply by email"
- if Gitlab.config.reply_by_email.enabled
+ if Gitlab.config.incoming_email.enabled
check_address_formatted_correctly
check_mail_room_config_exists
check_imap_authentication
@@ -662,12 +665,12 @@ namespace :gitlab do
def check_address_formatted_correctly
print "Address formatted correctly? ... "
- if Gitlab::ReplyByEmail.address_formatted_correctly?
+ if Gitlab::IncomingEmail.address_formatted_correctly?
puts "yes".green
else
puts "no".red
try_fixing_it(
- "Make sure that the address in config/gitlab.yml includes the '%{reply_key}' placeholder."
+ "Make sure that the address in config/gitlab.yml includes the '%{key}' placeholder."
)
fix_and_rerun
end
@@ -676,6 +679,11 @@ namespace :gitlab do
def check_initd_configured_correctly
print "Init.d configured correctly? ... "
+ if omnibus_gitlab?
+ puts 'skipped (omnibus-gitlab has no init script)'.magenta
+ return
+ end
+
path = "/etc/default/gitlab"
if File.exist?(path) && File.read(path).include?("mail_room_enabled=true")
@@ -686,7 +694,7 @@ namespace :gitlab do
"Enable mail_room in the init.d configuration."
)
for_more_information(
- "doc/reply_by_email/README.md"
+ "doc/incoming_email/README.md"
)
fix_and_rerun
end
@@ -705,7 +713,7 @@ namespace :gitlab do
"Enable mail_room in your Procfile."
)
for_more_information(
- "doc/reply_by_email/README.md"
+ "doc/incoming_email/README.md"
)
fix_and_rerun
end
@@ -750,7 +758,7 @@ namespace :gitlab do
"Check that the information in config/mail_room.yml is correct"
)
for_more_information(
- "doc/reply_by_email/README.md"
+ "doc/incoming_email/README.md"
)
fix_and_rerun
end
@@ -786,7 +794,7 @@ namespace :gitlab do
"Check that the information in config/mail_room.yml is correct"
)
for_more_information(
- "doc/reply_by_email/README.md"
+ "doc/incoming_email/README.md"
)
fix_and_rerun
end
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 6b1e3716147..9f5852ac613 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -46,43 +46,24 @@ namespace :gitlab do
desc "GitLab | Cleanup | Clean repositories"
task repos: :environment do
warn_user_is_not_gitlab
- remove_flag = ENV['REMOVE']
-
- git_base_path = Gitlab.config.gitlab_shell.repos_path
- all_dirs = Dir.glob(git_base_path + '/*')
-
- global_projects = Project.in_namespace(nil).pluck(:path)
-
- puts git_base_path.yellow
- puts "Looking for global repos to remove... "
-
- # skip non git repo
- all_dirs.select! do |dir|
- dir =~ /.git$/
- end
-
- # skip existing repos
- all_dirs.reject! do |dir|
- repo_name = File.basename dir
- path = repo_name.gsub(/\.git$/, "")
- global_projects.include?(path)
- end
- all_dirs.each do |dir_path|
- if remove_flag
- if FileUtils.rm_rf dir_path
- puts "Removed...#{dir_path}".red
- else
- puts "Cannot remove #{dir_path}".red
- end
- else
- puts "Can be removed: #{dir_path}".red
+ move_suffix = "+orphaned+#{Time.now.to_i}"
+ repo_root = Gitlab.config.gitlab_shell.repos_path
+ # Look for global repos (legacy, depth 1) and normal repos (depth 2)
+ IO.popen(%W(find #{repo_root} -mindepth 1 -maxdepth 2 -name *.git)) do |find|
+ find.each_line do |path|
+ path.chomp!
+ repo_with_namespace = path.
+ sub(repo_root, '').
+ sub(%r{^/*}, '').
+ chomp('.git').
+ chomp('.wiki')
+ next if Project.find_with_namespace(repo_with_namespace)
+ new_path = path + move_suffix
+ puts path.inspect + ' -> ' + new_path.inspect
+ File.rename(path, new_path)
end
end
-
- unless remove_flag
- puts "To cleanup this directories run this command with REMOVE=true".yellow
- end
end
desc "GitLab | Cleanup | Block users that have been removed in LDAP"
diff --git a/lib/tasks/services.rake b/lib/tasks/services.rake
new file mode 100644
index 00000000000..39541c0b9c6
--- /dev/null
+++ b/lib/tasks/services.rake
@@ -0,0 +1,98 @@
+services_template = <<-ERB
+# Services
+
+<% services.each do |service| %>
+## <%= service[:title] %>
+
+
+<% unless service[:description].blank? %>
+<%= service[:description] %>
+<% end %>
+
+
+### Create/Edit <%= service[:title] %> service
+
+Set <%= service[:title] %> service for a project.
+<% unless service[:help].blank? %>
+
+> <%= service[:help].gsub("\n", ' ') %>
+
+<% end %>
+
+```
+PUT /projects/:id/services/<%= service[:dashed_name] %>
+
+```
+
+Parameters:
+
+<% service[:params].each do |param| %>
+- `<%= param[:name] %>` <%= param[:required] ? "(**required**)" : "(optional)" %><%= [" -", param[:description]].join(" ").gsub("\n", '') unless param[:description].blank? %>
+
+<% end %>
+
+### Delete <%= service[:title] %> service
+
+Delete <%= service[:title] %> service for a project.
+
+```
+DELETE /projects/:id/services/<%= service[:dashed_name] %>
+
+```
+
+### Get <%= service[:title] %> service settings
+
+Get <%= service[:title] %> service settings for a project.
+
+```
+GET /projects/:id/services/<%= service[:dashed_name] %>
+
+```
+
+<% end %>
+ERB
+
+namespace :services do
+ task doc: :environment do
+ services = Service.available_services_names.map do |s|
+ service_start = Time.now
+ klass = "#{s}_service".classify.constantize
+
+ service = klass.new
+
+ service_hash = {}
+
+ service_hash[:title] = service.title
+ service_hash[:dashed_name] = s.dasherize
+ service_hash[:description] = service.description
+ service_hash[:help] = service.help
+ service_hash[:params] = service.fields.map do |p|
+ param_hash = {}
+
+ param_hash[:name] = p[:name]
+ param_hash[:description] = p[:placeholder] || p[:title]
+ param_hash[:required] = klass.validators_on(p[:name].to_sym).any? do |v|
+ v.class == ActiveRecord::Validations::PresenceValidator
+ end
+
+ param_hash
+ end.sort_by { |p| p[:required] ? 0 : 1 }
+
+ puts "Collected data for: #{service.title}, #{Time.now-service_start}"
+ service_hash
+ end
+
+ doc_start = Time.now
+ doc_path = File.join(Rails.root, 'doc', 'api', 'services.md')
+
+ result = ERB.new(services_template, 0 , '>')
+ .result(OpenStruct.new(services: services).instance_eval { binding })
+
+ File.open(doc_path, 'w') do |f|
+ f.write result
+ end
+
+ puts "write a new service.md to: #{doc_path.to_s}, #{Time.now-doc_start}"
+
+ end
+end
diff --git a/lib/tasks/spec.rake b/lib/tasks/spec.rake
index 831746815d7..365ff2defd4 100644
--- a/lib/tasks/spec.rake
+++ b/lib/tasks/spec.rake
@@ -19,11 +19,20 @@ namespace :spec do
run_commands(cmds)
end
+ desc 'GitLab | Rspec | Run benchmark specs'
+ task :benchmark do
+ cmds = [
+ %W(rake gitlab:setup),
+ %W(rspec spec --tag @benchmark)
+ ]
+ run_commands(cmds)
+ end
+
desc 'GitLab | Rspec | Run other specs'
task :other do
cmds = [
%W(rake gitlab:setup),
- %W(rspec spec --tag ~@api --tag ~@feature)
+ %W(rspec spec --tag ~@api --tag ~@feature --tag ~@benchmark)
]
run_commands(cmds)
end
@@ -33,7 +42,7 @@ desc "GitLab | Run specs"
task :spec do
cmds = [
%W(rake gitlab:setup),
- %W(rspec spec),
+ %W(rspec spec --tag ~@benchmark),
]
run_commands(cmds)
end
diff --git a/public/ci/build-canceled.svg b/public/ci/build-canceled.svg
new file mode 100644
index 00000000000..922e28bf696
--- /dev/null
+++ b/public/ci/build-canceled.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">canceled</text><text x="66" y="14">canceled</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-failed.svg b/public/ci/build-failed.svg
new file mode 100644
index 00000000000..1aefd3f1761
--- /dev/null
+++ b/public/ci/build-failed.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="78" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="78" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#e05d44" d="M37 0h41v20H37z"/><path fill="url(#b)" d="M0 0h78v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="56.5" y="15" fill="#010101" fill-opacity=".3">failed</text><text x="56.5" y="14">failed</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-pending.svg b/public/ci/build-pending.svg
new file mode 100644
index 00000000000..536931af84d
--- /dev/null
+++ b/public/ci/build-pending.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="92" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="92" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h55v20H37z"/><path fill="url(#b)" d="M0 0h92v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63.5" y="15" fill="#010101" fill-opacity=".3">pending</text><text x="63.5" y="14">pending</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-running.svg b/public/ci/build-running.svg
new file mode 100644
index 00000000000..0d71eef3c34
--- /dev/null
+++ b/public/ci/build-running.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="90" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="90" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#dfb317" d="M37 0h53v20H37z"/><path fill="url(#b)" d="M0 0h90v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="62.5" y="15" fill="#010101" fill-opacity=".3">running</text><text x="62.5" y="14">running</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-skipped.svg b/public/ci/build-skipped.svg
new file mode 100644
index 00000000000..f15507188e0
--- /dev/null
+++ b/public/ci/build-skipped.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="97" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="97" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h60v20H37z"/><path fill="url(#b)" d="M0 0h97v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66" y="15" fill="#010101" fill-opacity=".3">skipped</text><text x="66" y="14">skipped</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-success.svg b/public/ci/build-success.svg
new file mode 100644
index 00000000000..43b67e45f42
--- /dev/null
+++ b/public/ci/build-success.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="91" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="91" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#4c1" d="M37 0h54v20H37z"/><path fill="url(#b)" d="M0 0h91v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="63" y="15" fill="#010101" fill-opacity=".3">success</text><text x="63" y="14">success</text></g></svg> \ No newline at end of file
diff --git a/public/ci/build-unknown.svg b/public/ci/build-unknown.svg
new file mode 100644
index 00000000000..c72a2f5a7f5
--- /dev/null
+++ b/public/ci/build-unknown.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="98" height="20"><linearGradient id="b" x2="0" y2="100%"><stop offset="0" stop-color="#bbb" stop-opacity=".1"/><stop offset="1" stop-opacity=".1"/></linearGradient><mask id="a"><rect width="98" height="20" rx="3" fill="#fff"/></mask><g mask="url(#a)"><path fill="#555" d="M0 0h37v20H0z"/><path fill="#9f9f9f" d="M37 0h61v20H37z"/><path fill="url(#b)" d="M0 0h98v20H0z"/></g><g fill="#fff" text-anchor="middle" font-family="DejaVu Sans,Verdana,Geneva,sans-serif" font-size="11"><text x="18.5" y="15" fill="#010101" fill-opacity=".3">build</text><text x="18.5" y="14">build</text><text x="66.5" y="15" fill="#010101" fill-opacity=".3">unknown</text><text x="66.5" y="14">unknown</text></g></svg> \ No newline at end of file
diff --git a/public/ci/favicon.ico b/public/ci/favicon.ico
new file mode 100644
index 00000000000..9663d4d00b9
--- /dev/null
+++ b/public/ci/favicon.ico
Binary files differ
diff --git a/scripts/ci/prepare_build.sh b/scripts/ci/prepare_build.sh
new file mode 100755
index 00000000000..864a683a1bd
--- /dev/null
+++ b/scripts/ci/prepare_build.sh
@@ -0,0 +1,22 @@
+#!/bin/bash
+if [ -f /.dockerinit ]; then
+ export FLAGS=(--deployment --path /cache)
+
+ apt-get update -qq
+ apt-get install -y -qq nodejs
+
+ wget -q http://ftp.de.debian.org/debian/pool/main/p/phantomjs/phantomjs_1.9.0-1+b1_amd64.deb
+ dpkg -i phantomjs_1.9.0-1+b1_amd64.deb
+
+ cp config/database.yml.mysql config/database.yml
+ sed -i "s/username:.*/username: root/g" config/database.yml
+ sed -i "s/password:.*/password:/g" config/database.yml
+ sed -i "s/# socket:.*/host: mysql/g" config/database.yml
+else
+ export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
+
+ cp config/database.yml.mysql config/database.yml
+ sed -i "s/username\:.*$/username\: runner/" config/database.yml
+ sed -i "s/password\:.*$/password\: 'password'/" config/database.yml
+ sed -i "s/gitlab_ci_test/gitlab_ci_test_$((RANDOM/5000))/" config/database.yml
+fi
diff --git a/spec/benchmarks/models/user_spec.rb b/spec/benchmarks/models/user_spec.rb
new file mode 100644
index 00000000000..168be20b7a5
--- /dev/null
+++ b/spec/benchmarks/models/user_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe User, benchmark: true do
+ describe '.by_login' do
+ before do
+ %w{Alice Bob Eve}.each do |name|
+ create(:user,
+ email: "#{name}@gitlab.com",
+ username: name,
+ name: name)
+ end
+ end
+
+ let(:iterations) { 1000 }
+
+ describe 'using a capitalized username' do
+ benchmark_subject { User.by_login('Alice') }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+
+ describe 'using a lowercase username' do
+ benchmark_subject { User.by_login('alice') }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+
+ describe 'using a capitalized Email address' do
+ benchmark_subject { User.by_login('Alice@gitlab.com') }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+
+ describe 'using a lowercase Email address' do
+ benchmark_subject { User.by_login('alice@gitlab.com') }
+
+ it { is_expected.to iterate_per_second(iterations) }
+ end
+ end
+end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index c40b2c2a583..7168db117d6 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -7,6 +7,21 @@ describe Admin::UsersController do
sign_in(admin)
end
+ describe 'POST login_as' do
+ let(:user) { create(:user) }
+
+ it 'logs admin as another user' do
+ expect(warden.authenticate(scope: :user)).not_to eq(user)
+ post :login_as, id: user.username
+ expect(warden.authenticate(scope: :user)).to eq(user)
+ end
+
+ it 'redirects user to homepage' do
+ post :login_as, id: user.username
+ expect(response).to redirect_to(root_path)
+ end
+ end
+
describe 'DELETE #user with projects' do
let(:user) { create(:user) }
let(:project) { create(:project, namespace: user.namespace) }
diff --git a/spec/controllers/ci/commits_controller_spec.rb b/spec/controllers/ci/commits_controller_spec.rb
new file mode 100644
index 00000000000..cc39ce7687c
--- /dev/null
+++ b/spec/controllers/ci/commits_controller_spec.rb
@@ -0,0 +1,23 @@
+require "spec_helper"
+
+describe Ci::CommitsController do
+ describe "GET /status" do
+ it "returns status of commit" do
+ commit = FactoryGirl.create :ci_commit
+ get :status, id: commit.sha, ref_id: commit.ref, project_id: commit.project.id
+
+ expect(response).to be_success
+ expect(response.code).to eq('200')
+ JSON.parse(response.body)["status"] == "pending"
+ end
+
+ it "returns not_found status" do
+ commit = FactoryGirl.create :ci_commit
+ get :status, id: commit.sha, ref_id: "deploy", project_id: commit.project.id
+
+ expect(response).to be_success
+ expect(response.code).to eq('200')
+ JSON.parse(response.body)["status"] == "not_found"
+ end
+ end
+end
diff --git a/spec/controllers/import/fogbugz_controller_spec.rb b/spec/controllers/import/fogbugz_controller_spec.rb
new file mode 100644
index 00000000000..27b11267d2a
--- /dev/null
+++ b/spec/controllers/import/fogbugz_controller_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+require_relative 'import_spec_helper'
+
+describe Import::FogbugzController do
+ include ImportSpecHelper
+
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ describe 'GET status' do
+ before do
+ @repo = OpenStruct.new(name: 'vim')
+ stub_client(valid?: true)
+ end
+
+ it 'assigns variables' do
+ @project = create(:project, import_type: 'fogbugz', creator_id: user.id)
+ stub_client(repos: [@repo])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([@project])
+ expect(assigns(:repos)).to eq([@repo])
+ end
+
+ it 'does not show already added project' do
+ @project = create(:project, import_type: 'fogbugz', creator_id: user.id, import_source: 'vim')
+ stub_client(repos: [@repo])
+
+ get :status
+
+ expect(assigns(:already_added_projects)).to eq([@project])
+ expect(assigns(:repos)).to eq([])
+ end
+ end
+end
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
index 9c8619722cd..77436958711 100644
--- a/spec/controllers/namespaces_controller_spec.rb
+++ b/spec/controllers/namespaces_controller_spec.rb
@@ -46,13 +46,11 @@ describe NamespacesController do
context "when the project doesn't have public projects" do
context "when not signed in" do
- it "redirects to the sign in page" do
+ it "does not redirect to the sign in page" do
get :show, id: group.path
-
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).not_to redirect_to(new_user_session_path)
end
end
-
context "when signed in" do
before do
sign_in(user)
@@ -86,10 +84,10 @@ describe NamespacesController do
end
context "when the user doesn't have access to the project" do
- it "responds with status 404" do
+ it "redirects to the group's page" do
get :show, id: group.path
- expect(response.status).to eq(404)
+ expect(response).to redirect_to(group_path(group))
end
end
end
diff --git a/spec/controllers/profiles/two_factor_auths_controller_spec.rb b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
index f54706e3aa3..4fb1473c2d2 100644
--- a/spec/controllers/profiles/two_factor_auths_controller_spec.rb
+++ b/spec/controllers/profiles/two_factor_auths_controller_spec.rb
@@ -37,7 +37,7 @@ describe Profiles::TwoFactorAuthsController do
context 'with valid pin' do
before do
- expect(user).to receive(:valid_otp?).with(pin).and_return(true)
+ expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(true)
end
it 'sets two_factor_enabled' do
@@ -63,7 +63,7 @@ describe Profiles::TwoFactorAuthsController do
context 'with invalid pin' do
before do
- expect(user).to receive(:valid_otp?).with(pin).and_return(false)
+ expect(user).to receive(:validate_and_consume_otp!).with(pin).and_return(false)
end
it 'assigns error' do
diff --git a/spec/controllers/projects/compare_controller_spec.rb b/spec/controllers/projects/compare_controller_spec.rb
index b643b354073..2a447248b70 100644
--- a/spec/controllers/projects/compare_controller_spec.rb
+++ b/spec/controllers/projects/compare_controller_spec.rb
@@ -22,4 +22,30 @@ describe Projects::CompareController do
expect(assigns(:diffs).length).to be >= 1
expect(assigns(:commits).length).to be >= 1
end
+
+ describe 'non-existent refs' do
+ it 'invalid source ref' do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: 'non-existent',
+ to: ref_to)
+
+ expect(response).to be_success
+ expect(assigns(:diffs)).to eq([])
+ expect(assigns(:commits)).to eq([])
+ end
+
+ it 'invalid target ref' do
+ get(:show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ from: ref_from,
+ to: 'non-existent')
+
+ expect(response).to be_success
+ expect(assigns(:diffs)).to eq(nil)
+ expect(assigns(:commits)).to eq(nil)
+ end
+ end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 871b9219ca9..76d56bc989d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -8,28 +8,34 @@ describe Projects::IssuesController do
before do
sign_in(user)
project.team << [user, :developer]
- controller.instance_variable_set(:@project, project)
end
describe "GET #index" do
it "returns index" do
- get :index, namespace_id: project.namespace.id, project_id: project.id
+ get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(200)
end
+ it "return 301 if request path doesn't match project path" do
+ get :index, namespace_id: project.namespace.path, project_id: project.path.upcase
+
+ expect(response).to redirect_to(namespace_project_issues_path(project.namespace, project))
+ end
+
it "returns 404 when issues are disabled" do
project.issues_enabled = false
project.save
- get :index, namespace_id: project.namespace.id, project_id: project.id
+ get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(404)
end
it "returns 404 when external issue tracker is enabled" do
+ controller.instance_variable_set(:@project, project)
allow(project).to receive(:default_issues_tracker?).and_return(false)
- get :index, namespace_id: project.namespace.id, project_id: project.id
+ get :index, namespace_id: project.namespace.path, project_id: project.path
expect(response.status).to eq(404)
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index d3868c13202..8127efabe6e 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -5,6 +5,7 @@ describe Projects::MilestonesController do
let(:user) { create(:user) }
let(:milestone) { create(:milestone, project: project) }
let(:issue) { create(:issue, project: project, milestone: milestone) }
+ let(:merge_request) { create(:merge_request, source_project: project, target_project: project, milestone: milestone) }
before do
sign_in(user)
@@ -14,12 +15,21 @@ describe Projects::MilestonesController do
describe "#destroy" do
it "should remove milestone" do
+ merge_request.reload
expect(issue.milestone_id).to eq(milestone.id)
+
delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.id, format: :js
expect(response).to be_success
+
+ expect(Event.first.action).to eq(Event::DESTROYED)
+
expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound)
issue.reload
expect(issue.milestone_id).to eq(nil)
+
+ merge_request.reload
+ expect(merge_request.milestone_id).to eq(nil)
+
# Check system note left for milestone removal
last_note = project.issues.find(issue.id).notes[-1].note
expect(last_note).to eq('Milestone removed')
diff --git a/spec/controllers/projects/raw_controller_spec.rb b/spec/controllers/projects/raw_controller_spec.rb
new file mode 100644
index 00000000000..c114f342021
--- /dev/null
+++ b/spec/controllers/projects/raw_controller_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Projects::RawController do
+ let(:public_project) { create(:project, :public) }
+
+ describe "#show" do
+ context 'regular filename' do
+ let(:id) { 'master/README.md' }
+
+ it 'delivers ASCII file' do
+ get(:show,
+ namespace_id: public_project.namespace.to_param,
+ project_id: public_project.to_param,
+ id: id)
+
+ expect(response.status).to eq(200)
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(response.header['Content-Disposition']).
+ to eq("inline")
+ end
+ end
+
+ context 'image header' do
+ let(:id) { 'master/files/images/6049019_460s.jpg' }
+
+ it 'set image content type header' do
+ get(:show,
+ namespace_id: public_project.namespace.to_param,
+ project_id: public_project.to_param,
+ id: id)
+
+ expect(response.status).to eq(200)
+ expect(response.header['Content-Type']).to eq('image/jpeg')
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index 29233e9fae6..21beaf37fce 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -21,6 +21,20 @@ describe ProjectsController do
expect(response.body).to include("content='#{content}'")
end
end
+
+ context "when requested with case sensitive namespace and project path" do
+ it "redirects to the normalized path for case mismatch" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path.upcase
+
+ expect(response).to redirect_to("/#{public_project.path_with_namespace}")
+ end
+
+ it "loads the page if normalized path matches request path" do
+ get :show, namespace_id: public_project.namespace.path, id: public_project.path
+
+ expect(response.status).to eq(200)
+ end
+ end
end
describe "POST #toggle_star" do
diff --git a/spec/controllers/root_controller_spec.rb b/spec/controllers/root_controller_spec.rb
index abbbf6855fc..5a104ae7c99 100644
--- a/spec/controllers/root_controller_spec.rb
+++ b/spec/controllers/root_controller_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe RootController do
- describe 'GET show' do
+ describe 'GET index' do
context 'with a user' do
let(:user) { create(:user) }
@@ -10,21 +10,43 @@ describe RootController do
allow(subject).to receive(:current_user).and_return(user)
end
- context 'who has customized their dashboard setting' do
+ context 'who has customized their dashboard setting for starred projects' do
before do
user.update_attribute(:dashboard, 'stars')
end
it 'redirects to their specified dashboard' do
- get :show
+ get :index
expect(response).to redirect_to starred_dashboard_projects_path
end
end
+ context 'who has customized their dashboard setting for project activities' do
+ before do
+ user.update_attribute(:dashboard, 'project_activity')
+ end
+
+ it 'redirects to the activity list' do
+ get :index
+ expect(response).to redirect_to activity_dashboard_path
+ end
+ end
+
+ context 'who has customized their dashboard setting for starred project activities' do
+ before do
+ user.update_attribute(:dashboard, 'starred_project_activity')
+ end
+
+ it 'redirects to the activity list' do
+ get :index
+ expect(response).to redirect_to activity_dashboard_path(filter: 'starred')
+ end
+ end
+
context 'who uses the default dashboard setting' do
it 'renders the default dashboard' do
- get :show
- expect(response).to render_template 'dashboard/show'
+ get :index
+ expect(response).to render_template 'dashboard/projects/index'
end
end
end
diff --git a/spec/controllers/uploads_controller_spec.rb b/spec/controllers/uploads_controller_spec.rb
index 0f9780356b1..af5d043cf02 100644
--- a/spec/controllers/uploads_controller_spec.rb
+++ b/spec/controllers/uploads_controller_spec.rb
@@ -156,14 +156,6 @@ describe UploadsController do
end
context "when the project doesn't have public projects" do
- context "when not signed in" do
- it "redirects to the sign in page" do
- get :show, model: "group", mounted_as: "avatar", id: group.id, filename: "image.png"
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
context "when signed in" do
before do
sign_in(user)
diff --git a/spec/factories/abuse_reports.rb b/spec/factories/abuse_reports.rb
index 29fcbc5e197..8d287ded292 100644
--- a/spec/factories/abuse_reports.rb
+++ b/spec/factories/abuse_reports.rb
@@ -1,3 +1,15 @@
+# == Schema Information
+#
+# Table name: abuse_reports
+#
+# id :integer not null, primary key
+# reporter_id :integer
+# user_id :integer
+# message :text
+# created_at :datetime
+# updated_at :datetime
+#
+
# Read about factories at https://github.com/thoughtbot/factory_girl
FactoryGirl.define do
diff --git a/spec/factories/ci/builds.rb b/spec/factories/ci/builds.rb
new file mode 100644
index 00000000000..21b582afba4
--- /dev/null
+++ b/spec/factories/ci/builds.rb
@@ -0,0 +1,53 @@
+# == 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)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_build, class: Ci::Build do
+ ref 'master'
+ tag false
+ started_at 'Di 29. Okt 09:51:28 CET 2013'
+ finished_at 'Di 29. Okt 09:53:28 CET 2013'
+ commands 'ls -a'
+ options do
+ {
+ image: "ruby:2.1",
+ services: ["postgres"]
+ }
+ end
+
+ commit factory: :ci_commit
+
+ factory :ci_not_started_build do
+ started_at nil
+ finished_at nil
+ end
+
+ factory :ci_build_tag do
+ tag true
+ end
+ end
+end
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/commits.rb
new file mode 100644
index 00000000000..79e000b7ccb
--- /dev/null
+++ b/spec/factories/ci/commits.rb
@@ -0,0 +1,49 @@
+# == 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
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+FactoryGirl.define do
+ factory :ci_empty_commit, class: Ci::Commit do
+ sha '97de212e80737a608d939f648d959671fb0a0142'
+
+ gl_project factory: :empty_project
+
+ factory :ci_commit_without_jobs do
+ after(:build) do |commit|
+ allow(commit).to receive(:ci_yaml_file) { YAML.dump({}) }
+ end
+ end
+
+ factory :ci_commit_with_one_job do
+ after(:build) do |commit|
+ allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" } }) }
+ end
+ end
+
+ factory :ci_commit_with_two_jobs do
+ after(:build) do |commit|
+ allow(commit).to receive(:ci_yaml_file) { YAML.dump({ rspec: { script: "ls" }, spinach: { script: "ls" } }) }
+ end
+ end
+
+ factory :ci_commit do
+ after(:build) do |commit|
+ allow(commit).to receive(:ci_yaml_file) { File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml')) }
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/events.rb b/spec/factories/ci/events.rb
new file mode 100644
index 00000000000..9638618a400
--- /dev/null
+++ b/spec/factories/ci/events.rb
@@ -0,0 +1,24 @@
+# == 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
+#
+
+FactoryGirl.define do
+ factory :ci_event, class: Ci::Event do
+ sequence :description do |n|
+ "updated project settings#{n}"
+ end
+
+ factory :ci_admin_event do
+ is_admin true
+ end
+ end
+end
diff --git a/spec/factories/ci/projects.rb b/spec/factories/ci/projects.rb
new file mode 100644
index 00000000000..111e1a82816
--- /dev/null
+++ b/spec/factories/ci/projects.rb
@@ -0,0 +1,44 @@
+# == 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
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_project_without_token, class: Ci::Project do
+ default_ref 'master'
+
+ gl_project factory: :empty_project
+
+ factory :ci_project do
+ token 'iPWx6WM4lhHNedGfBpPJNP'
+ end
+
+ factory :ci_public_project do
+ public true
+ end
+ end
+end
diff --git a/spec/factories/ci/runner_projects.rb b/spec/factories/ci/runner_projects.rb
new file mode 100644
index 00000000000..3aa14ca434d
--- /dev/null
+++ b/spec/factories/ci/runner_projects.rb
@@ -0,0 +1,19 @@
+# == 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
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_runner_project, class: Ci::RunnerProject do
+ runner_id 1
+ project_id 1
+ end
+end
diff --git a/spec/factories/ci/runners.rb b/spec/factories/ci/runners.rb
new file mode 100644
index 00000000000..db759eca9ac
--- /dev/null
+++ b/spec/factories/ci/runners.rb
@@ -0,0 +1,38 @@
+# == 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)
+#
+
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_runner, class: Ci::Runner do
+ sequence :description do |n|
+ "My runner#{n}"
+ end
+
+ platform "darwin"
+
+ factory :ci_shared_runner do
+ is_shared true
+ end
+
+ factory :ci_specific_runner do
+ is_shared false
+ end
+ end
+end
diff --git a/spec/factories/ci/trigger_requests.rb b/spec/factories/ci/trigger_requests.rb
new file mode 100644
index 00000000000..db053c610cd
--- /dev/null
+++ b/spec/factories/ci/trigger_requests.rb
@@ -0,0 +1,13 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_trigger_request, class: Ci::TriggerRequest do
+ factory :ci_trigger_request_with_variables do
+ variables do
+ {
+ TRIGGER_KEY: 'TRIGGER_VALUE'
+ }
+ end
+ end
+ end
+end
diff --git a/spec/factories/ci/triggers.rb b/spec/factories/ci/triggers.rb
new file mode 100644
index 00000000000..fd3afdb1ec2
--- /dev/null
+++ b/spec/factories/ci/triggers.rb
@@ -0,0 +1,9 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :ci_trigger_without_token, class: Ci::Trigger do
+ factory :ci_trigger do
+ token 'token'
+ end
+ end
+end
diff --git a/spec/factories/ci/web_hook.rb b/spec/factories/ci/web_hook.rb
new file mode 100644
index 00000000000..40d878ecb3c
--- /dev/null
+++ b/spec/factories/ci/web_hook.rb
@@ -0,0 +1,6 @@
+FactoryGirl.define do
+ factory :ci_web_hook, class: Ci::WebHook do
+ sequence(:url) { FFaker::Internet.uri('http') }
+ project factory: :ci_project
+ end
+end
diff --git a/spec/factories/merge_requests.rb b/spec/factories/merge_requests.rb
index 3b7adfe4398..6080d0ccdef 100644
--- a/spec/factories/merge_requests.rb
+++ b/spec/factories/merge_requests.rb
@@ -19,6 +19,7 @@
# description :text
# position :integer default(0)
# locked_at :datetime
+# updated_by_id :integer
#
FactoryGirl.define do
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index e1009d5916e..9d777ddfccd 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -15,6 +15,7 @@
# noteable_id :integer
# system :boolean default(FALSE), not null
# st_diff :text
+# updated_by_id :integer
#
require_relative '../support/repo_helpers'
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 86717761582..c2c7364f6c5 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -111,6 +111,27 @@ describe "Admin::Users", feature: true do
expect(page).to have_content(@user.name)
end
+ describe 'Login as another user' do
+ it 'should show login button for other users and check that it works' do
+ another_user = create(:user)
+
+ visit admin_user_path(another_user)
+
+ click_link 'Log in as this user'
+
+ expect(page).to have_content("Logged in as #{another_user.username}")
+
+ page.within '.sidebar-user .username' do
+ expect(page).to have_content(another_user.username)
+ end
+ end
+
+ it 'should not show login button for admin itself' do
+ visit admin_user_path(@user)
+ expect(page).not_to have_content('Log in as this user')
+ end
+ end
+
describe 'Two-factor Authentication status' do
it 'shows when enabled' do
@user.update_attribute(:two_factor_enabled, true)
diff --git a/spec/features/atom/dashboard_spec.rb b/spec/features/atom/dashboard_spec.rb
index ad157d742ff..f81a3c117ff 100644
--- a/spec/features/atom/dashboard_spec.rb
+++ b/spec/features/atom/dashboard_spec.rb
@@ -6,7 +6,7 @@ describe "Dashboard Feed", feature: true do
context "projects atom feed via private token" do
it "should render projects atom feed" do
- visit dashboard_path(:atom, private_token: user.private_token)
+ visit dashboard_projects_path(:atom, private_token: user.private_token)
expect(body).to have_selector('feed title')
end
end
@@ -20,7 +20,7 @@ describe "Dashboard Feed", feature: true do
project.team << [user, :master]
issue_event(issue, user)
note_event(note, user)
- visit dashboard_path(:atom, private_token: user.private_token)
+ visit dashboard_projects_path(:atom, private_token: user.private_token)
end
it "should have issue opened event" do
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
new file mode 100644
index 00000000000..d0d60491b65
--- /dev/null
+++ b/spec/features/builds_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe "Builds" do
+
+ before do
+ login_as(:user)
+ @commit = FactoryGirl.create :ci_commit
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ @gl_project = @commit.project.gl_project
+ @gl_project.team << [@user, :master]
+ end
+
+ describe "GET /:project/builds/:id" do
+ before do
+ visit namespace_project_build_path(@gl_project.namespace, @gl_project, @build)
+ end
+
+ it { expect(page).to have_content @commit.sha[0..7] }
+ it { expect(page).to have_content @commit.git_commit_message }
+ it { expect(page).to have_content @commit.git_author_name }
+ end
+end
diff --git a/spec/features/ci/admin/builds_spec.rb b/spec/features/ci/admin/builds_spec.rb
new file mode 100644
index 00000000000..623d466c67b
--- /dev/null
+++ b/spec/features/ci/admin/builds_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe "Admin Builds" do
+ let(:commit) { FactoryGirl.create :ci_commit }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit }
+
+ before do
+ skip_ci_admin_auth
+ login_as :user
+ end
+
+ describe "GET /admin/builds" do
+ before do
+ build
+ visit ci_admin_builds_path
+ end
+
+ it { expect(page).to have_content "All builds" }
+ it { expect(page).to have_content build.short_sha }
+ end
+
+ describe "Tabs" do
+ it "shows all builds" do
+ FactoryGirl.create :ci_build, commit: commit, status: "pending"
+ FactoryGirl.create :ci_build, commit: commit, status: "running"
+ FactoryGirl.create :ci_build, commit: commit, status: "success"
+ FactoryGirl.create :ci_build, commit: commit, status: "failed"
+
+ visit ci_admin_builds_path
+
+ expect(page.all(".build-link").size).to eq(4)
+ end
+
+ it "shows pending builds" do
+ build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
+ build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
+ build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
+ build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+
+ visit ci_admin_builds_path
+
+ within ".nav.nav-tabs" do
+ click_on "Pending"
+ end
+
+ expect(page.find(".build-link")).to have_content(build.id)
+ expect(page.find(".build-link")).not_to have_content(build1.id)
+ expect(page.find(".build-link")).not_to have_content(build2.id)
+ expect(page.find(".build-link")).not_to have_content(build3.id)
+ end
+
+ it "shows running builds" do
+ build = FactoryGirl.create :ci_build, commit: commit, status: "pending"
+ build1 = FactoryGirl.create :ci_build, commit: commit, status: "running"
+ build2 = FactoryGirl.create :ci_build, commit: commit, status: "success"
+ build3 = FactoryGirl.create :ci_build, commit: commit, status: "failed"
+
+ visit ci_admin_builds_path
+
+ within ".nav.nav-tabs" do
+ click_on "Running"
+ end
+
+ expect(page.find(".build-link")).to have_content(build1.id)
+ expect(page.find(".build-link")).not_to have_content(build.id)
+ expect(page.find(".build-link")).not_to have_content(build2.id)
+ expect(page.find(".build-link")).not_to have_content(build3.id)
+ end
+ end
+end
diff --git a/spec/features/ci/admin/events_spec.rb b/spec/features/ci/admin/events_spec.rb
new file mode 100644
index 00000000000..a7e75cc4f6b
--- /dev/null
+++ b/spec/features/ci/admin/events_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe "Admin Events" do
+ let(:event) { FactoryGirl.create :ci_admin_event }
+
+ before do
+ skip_ci_admin_auth
+ login_as :user
+ end
+
+ describe "GET /admin/events" do
+ before do
+ event
+ visit ci_admin_events_path
+ end
+
+ it { expect(page).to have_content "Events" }
+ it { expect(page).to have_content event.description }
+ end
+end
diff --git a/spec/features/ci/admin/projects_spec.rb b/spec/features/ci/admin/projects_spec.rb
new file mode 100644
index 00000000000..b88f55a6807
--- /dev/null
+++ b/spec/features/ci/admin/projects_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe "Admin Projects" do
+ let(:project) { FactoryGirl.create :ci_project }
+
+ before do
+ skip_ci_admin_auth
+ login_as :user
+ end
+
+ describe "GET /admin/projects" do
+ before do
+ project
+ visit ci_admin_projects_path
+ end
+
+ it { expect(page).to have_content "Projects" }
+ end
+end
diff --git a/spec/features/ci/admin/runners_spec.rb b/spec/features/ci/admin/runners_spec.rb
new file mode 100644
index 00000000000..b83744f53a8
--- /dev/null
+++ b/spec/features/ci/admin/runners_spec.rb
@@ -0,0 +1,64 @@
+require 'spec_helper'
+
+describe "Admin Runners" do
+ before do
+ login_as :admin
+ end
+
+ describe "Runners page" do
+ before do
+ runner = FactoryGirl.create(:ci_runner)
+ commit = FactoryGirl.create(:ci_commit)
+ FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id)
+ visit ci_admin_runners_path
+ end
+
+ it { page.has_text? "Manage Runners" }
+ it { page.has_text? "To register a new runner" }
+ it { page.has_text? "Runners with last contact less than a minute ago: 1" }
+
+ describe 'search' do
+ before do
+ FactoryGirl.create :ci_runner, description: 'runner-foo'
+ FactoryGirl.create :ci_runner, description: 'runner-bar'
+
+ search_form = find('#runners-search')
+ search_form.fill_in 'search', with: 'runner-foo'
+ search_form.click_button 'Search'
+ end
+
+ it { expect(page).to have_content("runner-foo") }
+ it { expect(page).not_to have_content("runner-bar") }
+ end
+ end
+
+ describe "Runner show page" do
+ let(:runner) { FactoryGirl.create :ci_runner }
+
+ before do
+ @project1 = FactoryGirl.create(:ci_project)
+ @project2 = FactoryGirl.create(:ci_project)
+ visit ci_admin_runner_path(runner)
+ end
+
+ describe 'runner info' do
+ it { expect(find_field('runner_token').value).to eq runner.token }
+ end
+
+ describe 'projects' do
+ it { expect(page).to have_content(@project1.name_with_namespace) }
+ it { expect(page).to have_content(@project2.name_with_namespace) }
+ end
+
+ describe 'search' do
+ before do
+ search_form = find('#runner-projects-search')
+ search_form.fill_in 'search', with: @project1.gl_project.name
+ search_form.click_button 'Search'
+ end
+
+ it { expect(page).to have_content(@project1.name_with_namespace) }
+ it { expect(page).not_to have_content(@project2.name_with_namespace) }
+ end
+ end
+end
diff --git a/spec/features/ci/builds_spec.rb b/spec/features/ci/builds_spec.rb
new file mode 100644
index 00000000000..aa0df59c04d
--- /dev/null
+++ b/spec/features/ci/builds_spec.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe "Builds" do
+ before do
+ login_as(:user)
+ @commit = FactoryGirl.create :ci_commit
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ @gl_project = @commit.project.gl_project
+ @gl_project.team << [@user, :master]
+ end
+
+ describe "GET /:project/builds/:id/cancel" do
+ before do
+ @build.run!
+ visit cancel_ci_project_build_path(@commit.project, @build)
+ end
+
+ it { expect(page).to have_content 'canceled' }
+ it { expect(page).to have_content 'Retry' }
+ end
+
+ describe "POST /:project/builds/:id/retry" do
+ before do
+ visit cancel_ci_project_build_path(@commit.project, @build)
+ click_link 'Retry'
+ end
+
+ it { expect(page).to have_content 'pending' }
+ it { expect(page).to have_content 'Cancel' }
+ end
+end
diff --git a/spec/features/ci/events_spec.rb b/spec/features/ci/events_spec.rb
new file mode 100644
index 00000000000..5b9fd404159
--- /dev/null
+++ b/spec/features/ci/events_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe "Events" do
+ let(:user) { create(:user) }
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:event) { FactoryGirl.create :ci_admin_event, project: project }
+
+ before do
+ login_as(user)
+ project.gl_project.team << [user, :master]
+ end
+
+ describe "GET /ci/project/:id/events" do
+ before do
+ event
+ visit ci_project_events_path(project)
+ end
+
+ it { expect(page).to have_content "Events" }
+ it { expect(page).to have_content event.description }
+ end
+end
diff --git a/spec/features/ci/lint_spec.rb b/spec/features/ci/lint_spec.rb
new file mode 100644
index 00000000000..5d8f56e2cfb
--- /dev/null
+++ b/spec/features/ci/lint_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe "Lint" do
+ before do
+ login_as :user
+ end
+
+ it "Yaml parsing", js: true do
+ content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ visit ci_lint_path
+ fill_in "content", with: content
+ click_on "Validate"
+ within "table" do
+ expect(page).to have_content("Job - rspec")
+ expect(page).to have_content("Job - spinach")
+ expect(page).to have_content("Deploy Job - staging")
+ expect(page).to have_content("Deploy Job - production")
+ end
+ end
+
+ it "Yaml parsing with error", js: true do
+ visit ci_lint_path
+ fill_in "content", with: ""
+ click_on "Validate"
+ expect(page).to have_content("Status: syntax is incorrect")
+ expect(page).to have_content("Error: Please provide content of .gitlab-ci.yml")
+ end
+end
diff --git a/spec/features/ci/projects_spec.rb b/spec/features/ci/projects_spec.rb
new file mode 100644
index 00000000000..c633acf85fb
--- /dev/null
+++ b/spec/features/ci/projects_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe "Projects" do
+ let(:user) { create(:user) }
+
+ before do
+ login_as(user)
+ @project = FactoryGirl.create :ci_project, name: "GitLab / gitlab-shell"
+ @project.gl_project.team << [user, :master]
+ end
+
+ describe "GET /ci/projects/:id" do
+ before do
+ visit ci_project_path(@project)
+ end
+
+ it { expect(page).to have_content @project.name }
+ it { expect(page).to have_content 'All commits' }
+ end
+end
diff --git a/spec/features/ci_settings_spec.rb b/spec/features/ci_settings_spec.rb
new file mode 100644
index 00000000000..7e25e883018
--- /dev/null
+++ b/spec/features/ci_settings_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe "CI settings" do
+ let(:user) { create(:user) }
+ before { login_as(user) }
+
+ before do
+ @project = FactoryGirl.create :ci_project
+ @gl_project = @project.gl_project
+ @gl_project.team << [user, :master]
+ visit edit_namespace_project_ci_settings_path(@gl_project.namespace, @gl_project)
+ end
+
+ it { expect(page).to have_content 'Build Schedule' }
+
+ it "updates configuration" do
+ fill_in 'Timeout', with: '70'
+ click_button 'Save changes'
+ expect(page).to have_content 'was successfully updated'
+ expect(find_field('Timeout').value).to eq '70'
+ end
+end
diff --git a/spec/features/ci_web_hooks_spec.rb b/spec/features/ci_web_hooks_spec.rb
new file mode 100644
index 00000000000..efae0a42c1e
--- /dev/null
+++ b/spec/features/ci_web_hooks_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe 'CI web hooks' do
+ let(:user) { create(:user) }
+ before { login_as(user) }
+
+ before do
+ @project = FactoryGirl.create :ci_project
+ @gl_project = @project.gl_project
+ @gl_project.team << [user, :master]
+ visit namespace_project_ci_web_hooks_path(@gl_project.namespace, @gl_project)
+ end
+
+ context 'create a trigger' do
+ before do
+ fill_in 'web_hook_url', with: 'http://example.com'
+ click_on 'Add Web Hook'
+ end
+
+ it { expect(@project.web_hooks.count).to eq(1) }
+
+ it 'revokes the trigger' do
+ click_on 'Remove'
+ expect(@project.web_hooks.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
new file mode 100644
index 00000000000..5da220859e3
--- /dev/null
+++ b/spec/features/commits_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe "Commits" do
+ include CiStatusHelper
+
+ let(:project) { create(:project) }
+
+ describe "CI" do
+ before do
+ login_as :user
+ project.team << [@user, :master]
+ @ci_project = project.ensure_gitlab_ci_project
+ @commit = FactoryGirl.create :ci_commit, gl_project: project, sha: project.commit.sha
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ end
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
+
+ describe "GET /:project/commits/:sha" do
+ before do
+ visit ci_status_path(@commit)
+ end
+
+ it { expect(page).to have_content @commit.sha[0..7] }
+ it { expect(page).to have_content @commit.git_commit_message }
+ it { expect(page).to have_content @commit.git_author_name }
+ end
+
+ describe "Cancel commit" do
+ it "cancels commit" do
+ visit ci_status_path(@commit)
+ click_on "Cancel"
+ expect(page).to have_content "canceled"
+ end
+ end
+
+ describe ".gitlab-ci.yml not found warning" do
+ it "does not show warning" do
+ visit ci_status_path(@commit)
+ expect(page).not_to have_content ".gitlab-ci.yml not found in this commit"
+ end
+
+ it "shows warning" do
+ stub_ci_commit_yaml_file(nil)
+ visit ci_status_path(@commit)
+ expect(page).to have_content ".gitlab-ci.yml not found in this commit"
+ end
+ end
+ end
+end
diff --git a/spec/features/login_spec.rb b/spec/features/login_spec.rb
index cef432e512b..922c76285d1 100644
--- a/spec/features/login_spec.rb
+++ b/spec/features/login_spec.rb
@@ -95,7 +95,7 @@ feature 'Login', feature: true do
user = create(:user, password: 'not-the-default')
login_with(user)
- expect(page).to have_content('Invalid email or password.')
+ expect(page).to have_content('Invalid login or password.')
end
end
end
diff --git a/spec/features/password_reset_spec.rb b/spec/features/password_reset_spec.rb
index 2b6311e4fd7..85e70b4d47f 100644
--- a/spec/features/password_reset_spec.rb
+++ b/spec/features/password_reset_spec.rb
@@ -1,53 +1,43 @@
require 'spec_helper'
feature 'Password reset', feature: true do
- def forgot_password
- click_on 'Forgot your password?'
- fill_in 'Email', with: user.email
- click_button 'Reset password'
- user.reload
- end
-
- def get_reset_token
- mail = ActionMailer::Base.deliveries.last
- body = mail.body.encoded
- body.scan(/reset_password_token=(.+)\"/).flatten.first
- end
-
- def reset_password(password = 'password')
- visit edit_user_password_path(reset_password_token: get_reset_token)
+ describe 'throttling' do
+ it 'sends reset instructions when not previously sent' do
+ visit root_path
+ forgot_password(create(:user))
- fill_in 'New password', with: password
- fill_in 'Confirm new password', with: password
- click_button 'Change your password'
- end
+ expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
+ expect(current_path).to eq new_user_session_path
+ end
- describe 'with two-factor authentication' do
- let(:user) { create(:user, :two_factor) }
+ it 'sends reset instructions when previously sent more than a minute ago' do
+ user = create(:user)
+ user.send_reset_password_instructions
+ user.update_attribute(:reset_password_sent_at, 5.minutes.ago)
- it 'requires login after password reset' do
visit root_path
+ forgot_password(user)
- forgot_password
- reset_password
-
- expect(page).to have_content("Your password was changed successfully.")
- expect(page).not_to have_content("You are now signed in.")
+ expect(page).to have_content(I18n.t('devise.passwords.send_instructions'))
expect(current_path).to eq new_user_session_path
end
- end
- describe 'without two-factor authentication' do
- let(:user) { create(:user) }
+ it "throttles multiple resets in a short timespan" do
+ user = create(:user)
+ user.send_reset_password_instructions
- it 'automatically logs in after password reset' do
visit root_path
+ forgot_password(user)
- forgot_password
- reset_password
-
- expect(current_path).to eq root_path
- expect(page).to have_content("Your password was changed successfully. You are now signed in.")
+ expect(page).to have_content(I18n.t('devise.passwords.recently_reset'))
+ expect(current_path).to eq new_user_password_path
end
end
+
+ def forgot_password(user)
+ click_on 'Forgot your password?'
+ fill_in 'Email', with: user.email
+ click_button 'Reset password'
+ user.reload
+ end
end
diff --git a/spec/features/profiles/preferences_spec.rb b/spec/features/profiles/preferences_spec.rb
index 9bc6145dda4..8f645438cff 100644
--- a/spec/features/profiles/preferences_spec.rb
+++ b/spec/features/profiles/preferences_spec.rb
@@ -70,7 +70,7 @@ describe 'Profile > Preferences', feature: true do
expect(page.current_path).to eq starred_dashboard_projects_path
click_link 'Your Projects'
- expect(page.current_path).to eq dashboard_path
+ expect(page.current_path).to eq dashboard_projects_path
end
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
new file mode 100644
index 00000000000..06adb7633b2
--- /dev/null
+++ b/spec/features/runners_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe "Runners" do
+ include GitlabRoutingHelper
+
+ let(:user) { create(:user) }
+ before { login_as(user) }
+
+ describe "specific runners" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @project.gl_project.team << [user, :master]
+
+ @project2 = FactoryGirl.create :ci_project
+ @project2.gl_project.team << [user, :master]
+
+ @shared_runner = FactoryGirl.create :ci_shared_runner
+ @specific_runner = FactoryGirl.create :ci_specific_runner
+ @specific_runner2 = FactoryGirl.create :ci_specific_runner
+ @project.runners << @specific_runner
+ @project2.runners << @specific_runner2
+
+ visit runners_path(@project.gl_project)
+ end
+
+ it "places runners in right places" do
+ expect(page.find(".available-specific-runners")).to have_content(@specific_runner2.display_name)
+ expect(page.find(".activated-specific-runners")).to have_content(@specific_runner.display_name)
+ expect(page.find(".available-shared-runners")).to have_content(@shared_runner.display_name)
+ end
+
+ it "enables specific runner for project" do
+ within ".available-specific-runners" do
+ click_on "Enable for this project"
+ end
+
+ expect(page.find(".activated-specific-runners")).to have_content(@specific_runner2.display_name)
+ end
+
+ it "disables specific runner for project" do
+ @project2.runners << @specific_runner
+ visit runners_path(@project.gl_project)
+
+ within ".activated-specific-runners" do
+ click_on "Disable for this project"
+ end
+
+ expect(page.find(".available-specific-runners")).to have_content(@specific_runner.display_name)
+ end
+
+ it "removes specific runner for project if this is last project for that runners" do
+ within ".activated-specific-runners" do
+ click_on "Remove runner"
+ end
+
+ expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey
+ end
+ end
+
+ describe "shared runners" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @project.gl_project.team << [user, :master]
+ visit runners_path(@project.gl_project)
+ end
+
+ it "enables shared runners" do
+ click_on "Enable shared runners"
+ expect(@project.reload.shared_runners_enabled).to be_truthy
+ end
+ end
+
+ describe "show page" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @project.gl_project.team << [user, :master]
+ @specific_runner = FactoryGirl.create :ci_specific_runner
+ @project.runners << @specific_runner
+ visit runners_path(@project.gl_project)
+ end
+
+ it "shows runner information" do
+ click_on @specific_runner.short_sha
+ expect(page).to have_content(@specific_runner.platform)
+ end
+ end
+end
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index c38cddbb904..788581a26cb 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -4,7 +4,7 @@ describe "Dashboard access", feature: true do
include AccessMatchers
describe "GET /dashboard" do
- subject { dashboard_path }
+ subject { dashboard_projects_path }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for :user }
@@ -40,7 +40,7 @@ describe "Dashboard access", feature: true do
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for :visitor }
end
describe "GET /projects/new" do
diff --git a/spec/features/security/group_access_spec.rb b/spec/features/security/group_access_spec.rb
index 8ce15388605..4b78e3a61f0 100644
--- a/spec/features/security/group_access_spec.rb
+++ b/spec/features/security/group_access_spec.rb
@@ -68,7 +68,7 @@ describe 'Group access', feature: true do
it { is_expected.to be_allowed_for group_member(:guest) }
it { is_expected.to be_allowed_for :admin }
it { is_expected.to be_allowed_for :user }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for :visitor }
end
context 'with no projects' do
@@ -77,8 +77,8 @@ describe 'Group access', feature: true do
it { is_expected.to be_allowed_for group_member(:reporter) }
it { is_expected.to be_allowed_for group_member(:guest) }
it { is_expected.to be_allowed_for :admin }
- it { is_expected.to be_denied_for :user }
- it { is_expected.to be_denied_for :visitor }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_allowed_for :visitor }
end
end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
new file mode 100644
index 00000000000..69492d58878
--- /dev/null
+++ b/spec/features/triggers_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe 'Triggers' do
+ let(:user) { create(:user) }
+ before { login_as(user) }
+
+ before do
+ @project = FactoryGirl.create :ci_project
+ @gl_project = @project.gl_project
+ @gl_project.team << [user, :master]
+ visit namespace_project_triggers_path(@gl_project.namespace, @gl_project)
+ end
+
+ context 'create a trigger' do
+ before do
+ click_on 'Add Trigger'
+ expect(@project.triggers.count).to eq(1)
+ end
+
+ it 'contains trigger token' do
+ expect(page).to have_content(@project.triggers.first.token)
+ end
+
+ it 'revokes the trigger' do
+ click_on 'Revoke'
+ expect(@project.triggers.count).to eq(0)
+ end
+ end
+end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index efcb8a31abe..c1248162031 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
feature 'Users', feature: true do
+ let(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
+
scenario 'GET /users/sign_in creates a new user account' do
visit new_user_session_path
fill_in 'user_name', with: 'Name Surname'
@@ -11,7 +13,6 @@ feature 'Users', feature: true do
end
scenario 'Successful user signin invalidates password reset token' do
- user = create(:user)
expect(user.reset_password_token).to be_nil
visit new_user_password_path
@@ -28,7 +29,6 @@ feature 'Users', feature: true do
expect(user.reset_password_token).to be_nil
end
- let!(:user) { create(:user, username: 'user1', name: 'User 1', email: 'user1@gitlab.com') }
scenario 'Should show one error if email is already taken' do
visit new_user_session_path
fill_in 'user_name', with: 'Another user name'
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
new file mode 100644
index 00000000000..adb602f3edd
--- /dev/null
+++ b/spec/features/variables_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe "Variables" do
+ let(:user) { create(:user) }
+ before { login_as(user) }
+
+ describe "specific runners" do
+ before do
+ @project = FactoryGirl.create :ci_project
+ @gl_project = @project.gl_project
+ @gl_project.team << [user, :master]
+ end
+
+ it "creates variable", js: true do
+ visit namespace_project_variables_path(@gl_project.namespace, @gl_project)
+ click_on "Add a variable"
+ fill_in "Key", with: "SECRET_KEY"
+ fill_in "Value", with: "SECRET_VALUE"
+ click_on "Save changes"
+
+ expect(page).to have_content("Variables were successfully updated.")
+ expect(@project.variables.count).to eq(1)
+ end
+ end
+end
diff --git a/spec/helpers/ci_status_helper_spec.rb b/spec/helpers/ci_status_helper_spec.rb
new file mode 100644
index 00000000000..7fc53eb1472
--- /dev/null
+++ b/spec/helpers/ci_status_helper_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe CiStatusHelper do
+ include IconsHelper
+
+ let(:success_commit) { double("Ci::Commit", status: 'success') }
+ let(:failed_commit) { double("Ci::Commit", status: 'failed') }
+
+ describe 'ci_status_color' do
+ it { expect(ci_status_icon(success_commit)).to include('fa-check') }
+ it { expect(ci_status_icon(failed_commit)).to include('fa-close') }
+ end
+
+ describe 'ci_status_color' do
+ it { expect(ci_status_color(success_commit)).to eq('green') }
+ it { expect(ci_status_color(failed_commit)).to eq('red') }
+ end
+end
diff --git a/spec/helpers/gitlab_markdown_helper_spec.rb b/spec/helpers/gitlab_markdown_helper_spec.rb
index 0c2b3003092..5d471f2f6ee 100644
--- a/spec/helpers/gitlab_markdown_helper_spec.rb
+++ b/spec/helpers/gitlab_markdown_helper_spec.rb
@@ -41,6 +41,17 @@ describe GitlabMarkdownHelper do
expect(helper.markdown(actual)).to match(expected)
end
end
+
+ describe "override default project" do
+ let(:actual) { issue.to_reference }
+ let(:second_project) { create(:project) }
+ let(:second_issue) { create(:issue, project: second_project) }
+
+ it 'should link to the issue' do
+ expected = namespace_project_issue_path(second_project.namespace, second_project, second_issue)
+ expect(markdown(actual, project: second_project)).to match(expected)
+ end
+ end
end
describe '#link_to_gfm' do
@@ -98,6 +109,12 @@ describe GitlabMarkdownHelper do
act = helper.link_to_gfm(text, '/foo')
expect(act).to eq %Q(<a href="/foo">#{issues[0].to_reference}</a>)
end
+
+ it 'should replace commit message with emoji to link' do
+ actual = link_to_gfm(':book:Book', '/foo')
+ expect(actual).
+ to eq %Q(<img class="emoji" title=":book:" alt=":book:" src="http://localhost/assets/emoji/1F4D6.png" height="20" width="20" align="absmiddle"><a href="/foo">Book</a>)
+ end
end
describe '#render_wiki_content' do
@@ -138,4 +155,24 @@ describe GitlabMarkdownHelper do
expect(random_markdown_tip).to eq 'Random tip'
end
end
+
+ describe '#first_line_in_markdown' do
+ let(:text) { "@#{user.username}, can you look at this?\nHello world\n"}
+
+ it 'truncates Markdown properly' do
+ actual = first_line_in_markdown(text, 100, project: project)
+
+ doc = Nokogiri::HTML.parse(actual)
+
+ # Make sure we didn't create invalid markup
+ expect(doc.errors).to be_empty
+
+ # Leading user link
+ expect(doc.css('a').length).to eq(1)
+ expect(doc.css('a')[0].attr('href')).to eq user_path(user)
+ expect(doc.css('a')[0].text).to eq "@#{user.username}"
+
+ expect(doc.content).to eq "@#{user.username}, can you look at this?..."
+ end
+ end
end
diff --git a/spec/helpers/graph_helper_spec.rb b/spec/helpers/graph_helper_spec.rb
new file mode 100644
index 00000000000..4acf38771b7
--- /dev/null
+++ b/spec/helpers/graph_helper_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe GraphHelper do
+ describe '#get_refs' do
+ let(:project) { create(:project) }
+ let(:commit) { project.commit("master") }
+ let(:graph) { Network::Graph.new(project, 'master', commit, '') }
+
+ it 'filter our refs used by GitLab' do
+ allow(commit).to receive(:ref_names).and_return(['refs/merge-requests/abc', 'master', 'refs/tmp/xyz'])
+ self.instance_variable_set(:@graph, graph)
+ refs = get_refs(project.repository, commit)
+ expect(refs).to eq('master')
+ end
+ end
+end
diff --git a/spec/helpers/merge_requests_helper.rb b/spec/helpers/merge_requests_helper.rb
deleted file mode 100644
index 5262d644048..00000000000
--- a/spec/helpers/merge_requests_helper.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'spec_helper'
-
-describe MergeRequestsHelper do
- describe :issues_sentence do
- subject { issues_sentence(issues) }
- let(:issues) do
- [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)]
- end
-
- it { is_expected.to eq('#1, #2, and #3') }
- end
-end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
new file mode 100644
index 00000000000..0ef1efb8bce
--- /dev/null
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe MergeRequestsHelper do
+ describe "#issues_sentence" do
+ subject { issues_sentence(issues) }
+ let(:issues) do
+ [build(:issue, iid: 1), build(:issue, iid: 2), build(:issue, iid: 3)]
+ end
+
+ it { is_expected.to eq('#1, #2, and #3') }
+ end
+
+ describe "#format_mr_branch_names" do
+ describe "within the same project" do
+ let(:merge_request) { create(:merge_request) }
+ subject { format_mr_branch_names(merge_request) }
+
+ it { is_expected.to eq([merge_request.source_branch, merge_request.target_branch]) }
+ end
+
+ describe "within different projects" do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:merge_request) { create(:merge_request, source_project: fork_project, target_project: project) }
+ subject { format_mr_branch_names(merge_request) }
+ let(:source_title) { "#{fork_project.path_with_namespace}:#{merge_request.source_branch}" }
+ let(:target_title) { "#{project.path_with_namespace}:#{merge_request.target_branch}" }
+
+ it { is_expected.to eq([source_title, target_title]) }
+ end
+ end
+end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index 06f69262b71..e5df59c4fba 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -8,14 +8,18 @@ describe PreferencesHelper do
end
it 'raises an exception when defined choices may be using the wrong key' do
- expect(User).to receive(:dashboards).and_return(foo: 'foo', bar: 'bar')
+ dashboards = User.dashboards.dup
+ dashboards[:projects_changed] = dashboards.delete :projects
+ expect(User).to receive(:dashboards).and_return(dashboards)
expect { helper.dashboard_choices }.to raise_error(KeyError)
end
it 'provides better option descriptions' do
expect(helper.dashboard_choices).to match_array [
['Your Projects (default)', 'projects'],
- ['Starred Projects', 'stars']
+ ['Starred Projects', 'stars'],
+ ["Your Projects' Activity", 'project_activity'],
+ ["Starred Projects' Activity", 'starred_project_activity']
]
end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 99abb95d906..53e56ebff44 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -61,13 +61,13 @@ describe ProjectsHelper do
end
it "returns a valid cach key" do
- expect(helper.send(:readme_cache_key)).to eq("#{project.id}-#{project.commit.id}-readme")
+ expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-#{project.commit.id}-readme")
end
it "returns a valid cache key if HEAD does not exist" do
allow(project).to receive(:commit) { nil }
- expect(helper.send(:readme_cache_key)).to eq("#{project.id}-nil-readme")
+ expect(helper.send(:readme_cache_key)).to eq("#{project.path_with_namespace}-nil-readme")
end
end
end
diff --git a/spec/helpers/runners_helper_spec.rb b/spec/helpers/runners_helper_spec.rb
new file mode 100644
index 00000000000..b3d635a1932
--- /dev/null
+++ b/spec/helpers/runners_helper_spec.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe RunnersHelper do
+ it "returns - not contacted yet" do
+ runner = FactoryGirl.build :ci_runner
+ expect(runner_status_icon(runner)).to include("not connected yet")
+ end
+
+ it "returns offline text" do
+ runner = FactoryGirl.build(:ci_runner, contacted_at: 1.day.ago, active: true)
+ expect(runner_status_icon(runner)).to include("Runner is offline")
+ end
+
+ it "returns online text" do
+ runner = FactoryGirl.build(:ci_runner, contacted_at: 1.hour.ago, active: true)
+ expect(runner_status_icon(runner)).to include("Runner is online")
+ end
+end
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
new file mode 100644
index 00000000000..3f62527c5bb
--- /dev/null
+++ b/spec/helpers/time_helper_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe TimeHelper do
+ describe "#duration_in_words" do
+ it "returns minutes and seconds" do
+ intervals_in_words = {
+ 100 => "1 minute 40 seconds",
+ 121 => "2 minutes 1 second",
+ 3721 => "62 minutes 1 second",
+ 0 => "0 seconds"
+ }
+
+ intervals_in_words.each do |interval, expectation|
+ expect(duration_in_words(Time.now + interval, Time.now)).to eq(expectation)
+ end
+ end
+
+ it "calculates interval from now if there is no finished_at" do
+ expect(duration_in_words(nil, Time.now - 5)).to eq("5 seconds")
+ end
+ end
+
+ describe "#time_interval_in_words" do
+ it "returns minutes and seconds" do
+ intervals_in_words = {
+ 100 => "1 minute 40 seconds",
+ 121 => "2 minutes 1 second",
+ 3721 => "62 minutes 1 second",
+ 0 => "0 seconds"
+ }
+
+ intervals_in_words.each do |interval, expectation|
+ expect(time_interval_in_words(interval)).to eq(expectation)
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/syntax_highlight_spec.js.coffee b/spec/javascripts/syntax_highlight_spec.js.coffee
new file mode 100644
index 00000000000..6a73b6bf32c
--- /dev/null
+++ b/spec/javascripts/syntax_highlight_spec.js.coffee
@@ -0,0 +1,42 @@
+#= require syntax_highlight
+
+describe 'Syntax Highlighter', ->
+ stubUserColorScheme = (value) ->
+ window.gon ?= {}
+ window.gon.user_color_scheme = value
+
+ describe 'on a js-syntax-highlight element', ->
+ beforeEach ->
+ fixture.set('<div class="js-syntax-highlight"></div>')
+
+ it 'applies syntax highlighting', ->
+ stubUserColorScheme('monokai')
+
+ $('.js-syntax-highlight').syntaxHighlight()
+
+ expect($('.js-syntax-highlight')).toHaveClass('monokai')
+
+ describe 'on a parent element', ->
+ beforeEach ->
+ fixture.set """
+ <div class="parent">
+ <div class="js-syntax-highlight"></div>
+ <div class="foo"></div>
+ <div class="js-syntax-highlight"></div>
+ </div>
+ """
+
+ it 'applies highlighting to all applicable children', ->
+ stubUserColorScheme('monokai')
+
+ $('.parent').syntaxHighlight()
+
+ expect($('.parent, .foo')).not.toHaveClass('monokai')
+ expect($('.monokai').length).toBe(2)
+
+ it 'prevents an infinite loop when no matches exist', ->
+ fixture.set('<div></div>')
+
+ highlight = -> $('div').syntaxHighlight()
+
+ expect(highlight).not.toThrow()
diff --git a/spec/javascripts/zen_mode_spec.js.coffee b/spec/javascripts/zen_mode_spec.js.coffee
index 1f4ea58ad48..4cb3836755f 100644
--- a/spec/javascripts/zen_mode_spec.js.coffee
+++ b/spec/javascripts/zen_mode_spec.js.coffee
@@ -29,6 +29,11 @@ describe 'ZenMode', ->
enterZen()
expect(Mousetrap.pause).toHaveBeenCalled()
+ it 'removes textarea styling', ->
+ $('textarea').attr('style', 'height: 400px')
+ enterZen()
+ expect('textarea').not.toHaveAttr('style')
+
describe 'in use', ->
beforeEach ->
enterZen()
diff --git a/spec/lib/ci/ansi2html_spec.rb b/spec/lib/ci/ansi2html_spec.rb
new file mode 100644
index 00000000000..75c023bbc43
--- /dev/null
+++ b/spec/lib/ci/ansi2html_spec.rb
@@ -0,0 +1,134 @@
+require 'spec_helper'
+
+describe Ci::Ansi2html do
+ subject { Ci::Ansi2html }
+
+ it "prints non-ansi as-is" do
+ expect(subject.convert("Hello")).to eq('Hello')
+ end
+
+ it "strips non-color-changing controll sequences" do
+ expect(subject.convert("Hello \e[2Kworld")).to eq('Hello world')
+ end
+
+ it "prints simply red" do
+ expect(subject.convert("\e[31mHello\e[0m")).to eq('<span class="term-fg-red">Hello</span>')
+ end
+
+ it "prints simply red without trailing reset" do
+ expect(subject.convert("\e[31mHello")).to eq('<span class="term-fg-red">Hello</span>')
+ end
+
+ it "prints simply yellow" do
+ expect(subject.convert("\e[33mHello\e[0m")).to eq('<span class="term-fg-yellow">Hello</span>')
+ end
+
+ it "prints default on blue" do
+ expect(subject.convert("\e[39;44mHello")).to eq('<span class="term-bg-blue">Hello</span>')
+ end
+
+ it "prints red on blue" do
+ expect(subject.convert("\e[31;44mHello")).to eq('<span class="term-fg-red term-bg-blue">Hello</span>')
+ end
+
+ it "resets colors after red on blue" do
+ expect(subject.convert("\e[31;44mHello\e[0m world")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> world')
+ end
+
+ it "performs color change from red/blue to yellow/blue" do
+ expect(subject.convert("\e[31;44mHello \e[33mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-blue">world</span>')
+ end
+
+ it "performs color change from red/blue to yellow/green" do
+ expect(subject.convert("\e[31;44mHello \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-fg-yellow term-bg-green">world</span>')
+ end
+
+ it "performs color change from red/blue to reset to yellow/green" do
+ expect(subject.convert("\e[31;44mHello\e[0m \e[33;42mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello</span> <span class="term-fg-yellow term-bg-green">world</span>')
+ end
+
+ it "ignores unsupported codes" do
+ expect(subject.convert("\e[51mHello\e[0m")).to eq('Hello')
+ end
+
+ it "prints light red" do
+ expect(subject.convert("\e[91mHello\e[0m")).to eq('<span class="term-fg-l-red">Hello</span>')
+ end
+
+ it "prints default on light red" do
+ expect(subject.convert("\e[101mHello\e[0m")).to eq('<span class="term-bg-l-red">Hello</span>')
+ end
+
+ it "performs color change from red/blue to default/blue" do
+ expect(subject.convert("\e[31;44mHello \e[39mworld")).to eq('<span class="term-fg-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
+ end
+
+ it "performs color change from light red/blue to default/blue" do
+ expect(subject.convert("\e[91;44mHello \e[39mworld")).to eq('<span class="term-fg-l-red term-bg-blue">Hello </span><span class="term-bg-blue">world</span>')
+ end
+
+ it "prints bold text" do
+ expect(subject.convert("\e[1mHello")).to eq('<span class="term-bold">Hello</span>')
+ end
+
+ it "resets bold text" do
+ expect(subject.convert("\e[1mHello\e[21m world")).to eq('<span class="term-bold">Hello</span> world')
+ expect(subject.convert("\e[1mHello\e[22m world")).to eq('<span class="term-bold">Hello</span> world')
+ end
+
+ it "prints italic text" do
+ expect(subject.convert("\e[3mHello")).to eq('<span class="term-italic">Hello</span>')
+ end
+
+ it "resets italic text" do
+ expect(subject.convert("\e[3mHello\e[23m world")).to eq('<span class="term-italic">Hello</span> world')
+ end
+
+ it "prints underlined text" do
+ expect(subject.convert("\e[4mHello")).to eq('<span class="term-underline">Hello</span>')
+ end
+
+ it "resets underlined text" do
+ expect(subject.convert("\e[4mHello\e[24m world")).to eq('<span class="term-underline">Hello</span> world')
+ end
+
+ it "prints concealed text" do
+ expect(subject.convert("\e[8mHello")).to eq('<span class="term-conceal">Hello</span>')
+ end
+
+ it "resets concealed text" do
+ expect(subject.convert("\e[8mHello\e[28m world")).to eq('<span class="term-conceal">Hello</span> world')
+ end
+
+ it "prints crossed-out text" do
+ expect(subject.convert("\e[9mHello")).to eq('<span class="term-cross">Hello</span>')
+ end
+
+ it "resets crossed-out text" do
+ expect(subject.convert("\e[9mHello\e[29m world")).to eq('<span class="term-cross">Hello</span> world')
+ end
+
+ it "can print 256 xterm fg colors" do
+ expect(subject.convert("\e[38;5;16mHello")).to eq('<span class="xterm-fg-16">Hello</span>')
+ end
+
+ it "can print 256 xterm fg colors on normal magenta background" do
+ expect(subject.convert("\e[38;5;16;45mHello")).to eq('<span class="xterm-fg-16 term-bg-magenta">Hello</span>')
+ end
+
+ it "can print 256 xterm bg colors" do
+ expect(subject.convert("\e[48;5;240mHello")).to eq('<span class="xterm-bg-240">Hello</span>')
+ end
+
+ it "can print 256 xterm bg colors on normal magenta foreground" do
+ expect(subject.convert("\e[48;5;16;35mHello")).to eq('<span class="term-fg-magenta xterm-bg-16">Hello</span>')
+ end
+
+ it "prints bold colored text vividly" do
+ expect(subject.convert("\e[1;31mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ end
+
+ it "prints bold light colored text correctly" do
+ expect(subject.convert("\e[1;91mHello\e[0m")).to eq('<span class="term-fg-l-red term-bold">Hello</span>')
+ end
+end
diff --git a/spec/lib/ci/charts_spec.rb b/spec/lib/ci/charts_spec.rb
new file mode 100644
index 00000000000..83e2ad220b8
--- /dev/null
+++ b/spec/lib/ci/charts_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe "Charts" do
+
+ context "build_times" do
+ before do
+ @commit = FactoryGirl.create(:ci_commit)
+ FactoryGirl.create(:ci_build, commit: @commit)
+ end
+
+ it 'should return build times in minutes' do
+ chart = Ci::Charts::BuildTime.new(@commit.project)
+ expect(chart.build_times).to eq([2])
+ end
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
new file mode 100644
index 00000000000..aba957da488
--- /dev/null
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -0,0 +1,316 @@
+require 'spec_helper'
+
+module Ci
+ describe GitlabCiYamlProcessor do
+
+ describe "#builds_for_ref" do
+ let(:type) { 'test' }
+
+ it "returns builds if no branch specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec" }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref(type, "master").first).to eq({
+ stage: "test",
+ stage_idx: 1,
+ except: nil,
+ name: :rspec,
+ only: nil,
+ commands: "pwd\nrspec",
+ tag_list: [],
+ options: {},
+ allow_failure: false
+ })
+ end
+
+ it "does not return builds if only has another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["deploy"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
+
+ it "does not return builds if only has regexp with another branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["/^deploy$/"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(0)
+ end
+
+ it "returns builds if only has specified this branch" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", only: ["master"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "master").size).to eq(1)
+ end
+
+ it "does not build tags" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", except: ["tags"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "0-1", true).size).to eq(0)
+ end
+
+ it "returns builds if only has a list of branches including specified" do
+ config = YAML.dump({
+ before_script: ["pwd"],
+ rspec: { script: "rspec", type: type, only: ["master", "deploy"] }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ end
+
+ it "returns build only for specified type" do
+
+ config = YAML.dump({
+ before_script: ["pwd"],
+ build: { script: "build", type: "build", only: ["master", "deploy"] },
+ rspec: { script: "rspec", type: type, only: ["master", "deploy"] },
+ staging: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+ production: { script: "deploy", type: "deploy", only: ["master", "deploy"] },
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("production", "deploy").size).to eq(0)
+ expect(config_processor.builds_for_stage_and_ref(type, "deploy").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("deploy", "deploy").size).to eq(2)
+ end
+ end
+
+ describe "Image and service handling" do
+ it "returns image and service when defined" do
+ config = YAML.dump({
+ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: { script: "rspec" }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :rspec,
+ only: nil,
+ commands: "pwd\nrspec",
+ tag_list: [],
+ options: {
+ image: "ruby:2.1",
+ services: ["mysql"]
+ },
+ allow_failure: false
+ })
+ end
+
+ it "returns image and service when overridden for job" do
+ config = YAML.dump({
+ image: "ruby:2.1",
+ services: ["mysql"],
+ before_script: ["pwd"],
+ rspec: { image: "ruby:2.5", services: ["postgresql"], script: "rspec" }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+
+ expect(config_processor.builds_for_stage_and_ref("test", "master").size).to eq(1)
+ expect(config_processor.builds_for_stage_and_ref("test", "master").first).to eq({
+ except: nil,
+ stage: "test",
+ stage_idx: 1,
+ name: :rspec,
+ only: nil,
+ commands: "pwd\nrspec",
+ tag_list: [],
+ options: {
+ image: "ruby:2.5",
+ services: ["postgresql"]
+ },
+ allow_failure: false
+ })
+ end
+ end
+
+ describe "Variables" do
+ it "returns variables when defined" do
+ variables = {
+ var1: "value1",
+ var2: "value2",
+ }
+ config = YAML.dump({
+ variables: variables,
+ before_script: ["pwd"],
+ rspec: { script: "rspec" }
+ })
+
+ config_processor = GitlabCiYamlProcessor.new(config)
+ expect(config_processor.variables).to eq(variables)
+ end
+ end
+
+ describe "Error handling" do
+ it "indicates that object is invalid" do
+ expect{GitlabCiYamlProcessor.new("invalid_yaml\n!ccdvlf%612334@@@@")}.to raise_error(GitlabCiYamlProcessor::ValidationError)
+ end
+
+ it "returns errors if tags parameter is invalid" do
+ config = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: tags parameter should be an array of strings")
+ end
+
+ it "returns errors if before_script parameter is invalid" do
+ config = YAML.dump({ before_script: "bundle update", rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings")
+ end
+
+ it "returns errors if image parameter is invalid" do
+ config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "image should be a string")
+ end
+
+ it "returns errors if job image parameter is invalid" do
+ config = YAML.dump({ rspec: { script: "test", image: ["test"] } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: image should be a string")
+ end
+
+ it "returns errors if services parameter is not an array" do
+ config = YAML.dump({ services: "test", rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
+ end
+
+ it "returns errors if services parameter is not an array of strings" do
+ config = YAML.dump({ services: [10, "test"], rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "services should be an array of strings")
+ end
+
+ it "returns errors if job services parameter is not an array" do
+ config = YAML.dump({ rspec: { script: "test", services: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
+ end
+
+ it "returns errors if job services parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", services: [10, "test"] } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: services should be an array of strings")
+ end
+
+ it "returns errors if there are unknown parameters" do
+ config = YAML.dump({ extra: "bundle update" })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
+ end
+
+ it "returns errors if there are unknown parameters that are hashes, but doesn't have a script" do
+ config = YAML.dump({ extra: { services: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Unknown parameter: extra")
+ end
+
+ it "returns errors if there is no any jobs defined" do
+ config = YAML.dump({ before_script: ["bundle update"] })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "Please define at least one job")
+ end
+
+ it "returns errors if job allow_failure parameter is not an boolean" do
+ config = YAML.dump({ rspec: { script: "test", allow_failure: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: allow_failure parameter should be an boolean")
+ end
+
+ it "returns errors if job stage is not a string" do
+ config = YAML.dump({ rspec: { script: "test", type: 1, allow_failure: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+ end
+
+ it "returns errors if job stage is not a pre-defined stage" do
+ config = YAML.dump({ rspec: { script: "test", type: "acceptance", allow_failure: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test, deploy")
+ end
+
+ it "returns errors if job stage is not a defined stage" do
+ config = YAML.dump({ types: ["build", "test"], rspec: { script: "test", type: "acceptance", allow_failure: "string" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: stage parameter should be build, test")
+ end
+
+ it "returns errors if stages is not an array" do
+ config = YAML.dump({ types: "test", rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings")
+ end
+
+ it "returns errors if stages is not an array of strings" do
+ config = YAML.dump({ types: [true, "test"], rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "stages should be an array of strings")
+ end
+
+ it "returns errors if variables is not a map" do
+ config = YAML.dump({ variables: "test", rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
+ end
+
+ it "returns errors if variables is not a map of key-valued strings" do
+ config = YAML.dump({ variables: { test: false }, rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "variables should be a map of key-valued strings")
+ end
+ end
+ end
+end
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 9c115bbfc6a..48bc60eed16 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -3,7 +3,7 @@ require 'spec_helper'
describe ExtractsPath do
include ExtractsPath
include RepoHelpers
- include Rails.application.routes.url_helpers
+ include Gitlab::Application.routes.url_helpers
let(:project) { double('project') }
diff --git a/spec/lib/gitlab/backend/grack_auth_spec.rb b/spec/lib/gitlab/backend/grack_auth_spec.rb
index d9676445908..37c527221a0 100644
--- a/spec/lib/gitlab/backend/grack_auth_spec.rb
+++ b/spec/lib/gitlab/backend/grack_auth_spec.rb
@@ -151,14 +151,14 @@ describe Grack::Auth do
end
it "repeated attempts followed by successful attempt" do
- for n in 0..maxretry do
+ maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
expect(attempt_login(true)).to eq(200)
expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
- for n in 0..maxretry do
+ maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
end
@@ -175,12 +175,14 @@ describe Grack::Auth do
context "when a gitlab ci token is provided" do
let(:token) { "123" }
+ let(:gitlab_ci_project) { FactoryGirl.create :ci_project, token: token }
before do
+ project.gitlab_ci_project = gitlab_ci_project
+ project.save
+
gitlab_ci_service = project.build_gitlab_ci_service
gitlab_ci_service.active = true
- gitlab_ci_service.token = token
- gitlab_ci_service.project_url = "http://google.com"
gitlab_ci_service.save
env["HTTP_AUTHORIZATION"] = ActionController::HttpAuthentication::Basic.encode_credentials("gitlab-ci-token", token)
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 1cc80f35f98..e470b7cd5f5 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -2,7 +2,7 @@ require "spec_helper"
describe Gitlab::Email::Receiver do
before do
- stub_reply_by_email_setting(enabled: true, address: "reply+%{reply_key}@appmail.adventuretime.ooo")
+ stub_incoming_email_setting(enabled: true, address: "reply+%{key}@appmail.adventuretime.ooo")
end
let(:reply_key) { "59d8df8370b7e95c5a49fbf86aeb2c93" }
diff --git a/spec/lib/gitlab/incoming_email_spec.rb b/spec/lib/gitlab/incoming_email_spec.rb
new file mode 100644
index 00000000000..5fdb9c723b1
--- /dev/null
+++ b/spec/lib/gitlab/incoming_email_spec.rb
@@ -0,0 +1,61 @@
+require "spec_helper"
+
+describe Gitlab::IncomingEmail do
+ describe "self.enabled?" do
+ context "when reply by email is enabled" do
+ before do
+ stub_incoming_email_setting(enabled: true)
+ end
+
+ context "when the address is valid" do
+ before do
+ stub_incoming_email_setting(address: "replies+%{key}@example.com")
+ end
+
+ it "returns true" do
+ expect(described_class.enabled?).to be_truthy
+ end
+ end
+
+ context "when the address is invalid" do
+ before do
+ stub_incoming_email_setting(address: "replies@example.com")
+ end
+
+ it "returns false" do
+ expect(described_class.enabled?).to be_falsey
+ end
+ end
+ end
+
+ context "when reply by email is disabled" do
+ before do
+ stub_incoming_email_setting(enabled: false)
+ end
+
+ it "returns false" do
+ expect(described_class.enabled?).to be_falsey
+ end
+ end
+ end
+
+ context "self.reply_address" do
+ before do
+ stub_incoming_email_setting(address: "replies+%{key}@example.com")
+ end
+
+ it "returns the address with an interpolated reply key" do
+ expect(described_class.reply_address("key")).to eq("replies+key@example.com")
+ end
+ end
+
+ context "self.key_from_address" do
+ before do
+ stub_incoming_email_setting(address: "replies+%{key}@example.com")
+ end
+
+ it "returns reply key" do
+ expect(described_class.key_from_address("replies+key@example.com")).to eq("key")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/ldap/auth_hash_spec.rb
new file mode 100644
index 00000000000..7d8268536a4
--- /dev/null
+++ b/spec/lib/gitlab/ldap/auth_hash_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Gitlab::LDAP::AuthHash do
+ let(:auth_hash) do
+ Gitlab::LDAP::AuthHash.new(
+ OmniAuth::AuthHash.new(
+ uid: '123456',
+ provider: 'ldapmain',
+ info: info,
+ extra: {
+ raw_info: raw_info
+ }
+ )
+ )
+ end
+
+ let(:info) do
+ {
+ name: 'Smith, J.',
+ email: 'johnsmith@example.com',
+ nickname: '123456'
+ }
+ end
+
+ let(:raw_info) do
+ {
+ uid: ['123456'],
+ email: ['johnsmith@example.com'],
+ cn: ['Smith, J.'],
+ fullName: ['John Smith']
+ }
+ end
+
+ context "without overridden attributes" do
+
+ it "has the correct username" do
+ expect(auth_hash.username).to eq("123456")
+ end
+
+ it "has the correct name" do
+ expect(auth_hash.name).to eq("Smith, J.")
+ end
+ end
+
+ context "with overridden attributes" do
+ let(:attributes) do
+ {
+ 'username' => ['mail', 'email'],
+ 'name' => 'fullName'
+ }
+ end
+
+ before do
+ allow_any_instance_of(Gitlab::LDAP::Config).to receive(:attributes).and_return(attributes)
+ end
+
+ it "has the correct username" do
+ expect(auth_hash.username).to eq("johnsmith@example.com")
+ end
+
+ it "has the correct name" do
+ expect(auth_hash.name).to eq("John Smith")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/ldap/user_spec.rb
index 84d9fb54b61..fd2e5f6d0e1 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/ldap/user_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::LDAP::User do
}
end
let(:auth_hash) do
- double(uid: 'my-uid', provider: 'ldapmain', info: double(info))
+ OmniAuth::AuthHash.new(uid: 'my-uid', provider: 'ldapmain', info: info)
end
describe :changed? do
diff --git a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb
index 7f4d67e403f..027336ceb73 100644
--- a/spec/lib/gitlab/markdown/relative_link_filter_spec.rb
+++ b/spec/lib/gitlab/markdown/relative_link_filter_spec.rb
@@ -4,14 +4,16 @@ require 'spec_helper'
module Gitlab::Markdown
describe RelativeLinkFilter do
- def filter(doc)
- described_class.call(doc, {
+ def filter(doc, contexts = {})
+ contexts.reverse_merge!({
commit: project.commit,
project: project,
project_wiki: project_wiki,
ref: ref,
requested_path: requested_path
})
+
+ described_class.call(doc, contexts)
end
def image(path)
@@ -75,6 +77,22 @@ module Gitlab::Markdown
to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
end
+ it 'rebuilds relative URL for a file in the repo up one directory' do
+ relative_link = link('../api/README.md')
+ doc = filter(relative_link, requested_path: 'doc/update/7.14-to-8.0.md')
+
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
+ it 'rebuilds relative URL for a file in the repo up multiple directories' do
+ relative_link = link('../../../api/README.md')
+ doc = filter(relative_link, requested_path: 'doc/foo/bar/baz/README.md')
+
+ expect(doc.at_css('a')['href']).
+ to eq "/#{project_path}/blob/#{ref}/doc/api/README.md"
+ end
+
it 'rebuilds relative URL for a file in the repo with an anchor' do
doc = filter(link('README.md#section'))
expect(doc.at_css('a')['href']).
@@ -108,8 +126,8 @@ module Gitlab::Markdown
escaped = Addressable::URI.escape(path)
# Stub these methods so the file doesn't actually need to be in the repo
- allow_any_instance_of(described_class).to receive(:file_exists?).
- and_return(true)
+ allow_any_instance_of(described_class).
+ to receive(:file_exists?).and_return(true)
allow_any_instance_of(described_class).
to receive(:image?).with(path).and_return(true)
diff --git a/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb b/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb
new file mode 100644
index 00000000000..6a490673728
--- /dev/null
+++ b/spec/lib/gitlab/markdown/syntax_highlight_filter_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+module Gitlab::Markdown
+ describe SyntaxHighlightFilter do
+ include FilterSpecHelper
+
+ it 'highlights valid code blocks' do
+ result = filter('<pre><code>def fun end</code>')
+ expect(result.to_html).to eq("<pre class=\"code highlight js-syntax-highlight plaintext\"><code>def fun end</code></pre>\n")
+ end
+
+ it 'passes through invalid code blocks' do
+ allow_any_instance_of(SyntaxHighlightFilter).to receive(:block_code).and_raise(StandardError)
+
+ result = filter('<pre><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre>This is a test</pre>')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
index e4a6cd954cc..5632f2306ec 100644
--- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/o_auth/auth_hash_spec.rb
@@ -3,11 +3,11 @@ require 'spec_helper'
describe Gitlab::OAuth::AuthHash do
let(:auth_hash) do
Gitlab::OAuth::AuthHash.new(
- double({
+ OmniAuth::AuthHash.new(
provider: provider_ascii,
uid: uid_ascii,
- info: double(info_hash)
- })
+ info: info_hash
+ )
)
end
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/o_auth/user_spec.rb
index c6cca98a037..fd3ab1fb7c8 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/o_auth/user_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::OAuth::User do
let(:gl_user) { oauth_user.gl_user }
let(:uid) { 'my-uid' }
let(:provider) { 'my-provider' }
- let(:auth_hash) { double(uid: uid, provider: provider, info: double(info_hash)) }
+ let(:auth_hash) { OmniAuth::AuthHash.new(uid: uid, provider: provider, info: info_hash) }
let(:info_hash) do
{
nickname: '-john+gitlab-ETC%.git@gmail.com',
@@ -19,10 +19,6 @@ describe Gitlab::OAuth::User do
let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
it "finds an existing user based on uid and provider (facebook)" do
- # FIXME (rspeicher): It's unlikely that this test is actually doing anything
- # `auth` is never used and removing it entirely doesn't break the test, so
- # what's it doing?
- auth = double(info: double(name: 'John'), uid: 'my-uid', provider: 'my-provider')
expect( oauth_user.persisted? ).to be_truthy
end
diff --git a/spec/lib/gitlab/reply_by_email_spec.rb b/spec/lib/gitlab/reply_by_email_spec.rb
deleted file mode 100644
index a678c7e1a76..00000000000
--- a/spec/lib/gitlab/reply_by_email_spec.rb
+++ /dev/null
@@ -1,86 +0,0 @@
-require "spec_helper"
-
-describe Gitlab::ReplyByEmail do
- describe "self.enabled?" do
- context "when reply by email is enabled" do
- before do
- stub_reply_by_email_setting(enabled: true)
- end
-
- context "when the address is valid" do
- before do
- stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com")
- end
-
- it "returns true" do
- expect(described_class.enabled?).to be_truthy
- end
- end
-
- context "when the address is invalid" do
- before do
- stub_reply_by_email_setting(address: "replies@example.com")
- end
-
- it "returns false" do
- expect(described_class.enabled?).to be_falsey
- end
- end
- end
-
- context "when reply by email is disabled" do
- before do
- stub_reply_by_email_setting(enabled: false)
- end
-
- it "returns false" do
- expect(described_class.enabled?).to be_falsey
- end
- end
- end
-
- describe "self.reply_key" do
- context "when enabled" do
- before do
- allow(described_class).to receive(:enabled?).and_return(true)
- end
-
- it "returns a random hex" do
- key = described_class.reply_key
- key2 = described_class.reply_key
-
- expect(key).not_to eq(key2)
- end
- end
-
- context "when disabled" do
- before do
- allow(described_class).to receive(:enabled?).and_return(false)
- end
-
- it "returns nil" do
- expect(described_class.reply_key).to be_nil
- end
- end
- end
-
- context "self.reply_address" do
- before do
- stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com")
- end
-
- it "returns the address with an interpolated reply key" do
- expect(described_class.reply_address("key")).to eq("replies+key@example.com")
- end
- end
-
- context "self.reply_key_from_address" do
- before do
- stub_reply_by_email_setting(address: "replies+%{reply_key}@example.com")
- end
-
- it "returns reply key" do
- expect(described_class.reply_key_from_address("replies+key@example.com")).to eq("key")
- end
- end
-end
diff --git a/spec/mailers/ci/notify_spec.rb b/spec/mailers/ci/notify_spec.rb
new file mode 100644
index 00000000000..b83fb41603b
--- /dev/null
+++ b/spec/mailers/ci/notify_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Ci::Notify do
+ include EmailSpec::Helpers
+ include EmailSpec::Matchers
+
+ before do
+ @commit = FactoryGirl.create :ci_commit
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ end
+
+ describe 'build success' do
+ subject { Ci::Notify.build_success_email(@build.id, 'wow@example.com') }
+
+ it 'has the correct subject' do
+ should have_subject /Build success for/
+ end
+
+ it 'contains name of project' do
+ should have_body_text /build successful/
+ end
+ end
+
+ describe 'build fail' do
+ subject { Ci::Notify.build_fail_email(@build.id, 'wow@example.com') }
+
+ it 'has the correct subject' do
+ should have_subject /Build failed for/
+ end
+
+ it 'contains name of project' do
+ should have_body_text /build failed/
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 97c07ad7d55..cb67ec95d57 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -52,6 +52,7 @@ describe Notify do
end
it 'has headers that reference an existing thread' do
+ is_expected.to have_header 'Message-ID', /<(.*)@#{Gitlab.config.gitlab.host}>/
is_expected.to have_header 'References', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
is_expected.to have_header 'In-Reply-To', /<#{thread_id_prefix}(.*)@#{Gitlab.config.gitlab.host}>/
is_expected.to have_header 'X-GitLab-Project', /#{project.name}/
@@ -399,7 +400,7 @@ describe Notify do
describe 'project was moved' do
let(:project) { create(:project) }
let(:user) { create(:user) }
- subject { Notify.project_was_moved_email(project.id, user.id) }
+ subject { Notify.project_was_moved_email(project.id, user.id, "gitlab/gitlab") }
it_behaves_like 'an email sent from GitLab'
@@ -712,7 +713,7 @@ describe Notify do
before do
user.update_attribute(:email, "user@company.com")
- user.confirm!
+ user.confirm
end
it "is sent from the committer email" do
@@ -730,7 +731,7 @@ describe Notify do
before do
user.update_attribute(:email, "user@something.company.com")
- user.confirm!
+ user.confirm
end
it "is sent from the default email" do
@@ -748,7 +749,7 @@ describe Notify do
before do
user.update_attribute(:email, "user@mpany.com")
- user.confirm!
+ user.confirm
end
it "is sent from the default email" do
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index d83004a8388..d45319b25d4 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -1,7 +1,31 @@
+# == Schema Information
+#
+# Table name: abuse_reports
+#
+# id :integer not null, primary key
+# reporter_id :integer
+# user_id :integer
+# message :text
+# created_at :datetime
+# updated_at :datetime
+#
+
require 'rails_helper'
RSpec.describe AbuseReport, type: :model do
subject { create(:abuse_report) }
it { expect(subject).to be_valid }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:reporter).class_name('User') }
+ it { is_expected.to belong_to(:user) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:reporter) }
+ it { is_expected.to validate_presence_of(:user) }
+ it { is_expected.to validate_presence_of(:message) }
+ it { is_expected.to validate_uniqueness_of(:user_id) }
+ end
end
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index bc14ff98fd8..de0b2ef4cda 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -22,6 +22,7 @@
# user_oauth_applications :boolean default(TRUE)
# after_sign_out_path :string(255)
# session_expire_delay :integer default(10080), not null
+# import_sources :text
#
require 'spec_helper'
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 8ab72151a69..d80748f23a4 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -27,12 +27,12 @@ describe BroadcastMessage do
end
it "should return nil if time not come" do
- broadcast_message = create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days)
+ create(:broadcast_message, starts_at: Time.now.tomorrow, ends_at: Time.now + 2.days)
expect(BroadcastMessage.current).to be_nil
end
it "should return nil if time has passed" do
- broadcast_message = create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday)
+ create(:broadcast_message, starts_at: Time.now - 2.days, ends_at: Time.now.yesterday)
expect(BroadcastMessage.current).to be_nil
end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
new file mode 100644
index 00000000000..da56f6e31ae
--- /dev/null
+++ b/spec/models/ci/build_spec.rb
@@ -0,0 +1,387 @@
+# == 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)
+# deploy :boolean default(FALSE)
+# options :text
+# allow_failure :boolean default(FALSE), not null
+# stage :string(255)
+# trigger_request_id :integer
+#
+
+require 'spec_helper'
+
+describe Ci::Build do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit }
+ subject { build }
+
+ it { is_expected.to belong_to(:commit) }
+ it { is_expected.to belong_to(:user) }
+ it { is_expected.to validate_presence_of :status }
+ it { is_expected.to validate_presence_of :ref }
+
+ it { is_expected.to respond_to :success? }
+ it { is_expected.to respond_to :failed? }
+ it { is_expected.to respond_to :running? }
+ it { is_expected.to respond_to :pending? }
+ it { is_expected.to respond_to :trace_html }
+
+ describe :first_pending do
+ let(:first) { FactoryGirl.create :ci_build, commit: commit, status: 'pending', created_at: Date.yesterday }
+ let(:second) { FactoryGirl.create :ci_build, commit: commit, status: 'pending' }
+ before { first; second }
+ subject { Ci::Build.first_pending }
+
+ it { is_expected.to be_a(Ci::Build) }
+ it('returns with the first pending build') { is_expected.to eq(first) }
+ end
+
+ describe :create_from do
+ before do
+ build.status = 'success'
+ build.save
+ end
+ let(:create_from_build) { Ci::Build.create_from build }
+
+ it 'there should be a pending task' do
+ expect(Ci::Build.pending.count(:all)).to eq 0
+ create_from_build
+ expect(Ci::Build.pending.count(:all)).to be > 0
+ end
+ end
+
+ describe :started? do
+ subject { build.started? }
+
+ context 'without started_at' do
+ before { build.started_at = nil }
+
+ it { is_expected.to be_falsey }
+ end
+
+ %w(running success failed).each do |status|
+ context "if build status is #{status}" do
+ before { build.status = status }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(pending canceled).each do |status|
+ context "if build status is #{status}" do
+ before { build.status = status }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :active? do
+ subject { build.active? }
+
+ %w(pending running).each do |state|
+ context "if build.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(success failed canceled).each do |state|
+ context "if build.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :complete? do
+ subject { build.complete? }
+
+ %w(success failed canceled).each do |state|
+ context "if build.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ %w(pending running).each do |state|
+ context "if build.status is #{state}" do
+ before { build.status = state }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ describe :ignored? do
+ subject { build.ignored? }
+
+ context 'if build is not allowed to fail' do
+ before { build.allow_failure = false }
+
+ context 'and build.status is success' do
+ before { build.status = 'success' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and build.status is failed' do
+ before { build.status = 'failed' }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ context 'if build is allowed to fail' do
+ before { build.allow_failure = true }
+
+ context 'and build.status is success' do
+ before { build.status = 'success' }
+
+ it { is_expected.to be_falsey }
+ end
+
+ context 'and build.status is failed' do
+ before { build.status = 'failed' }
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ describe :trace do
+ subject { build.trace_html }
+
+ it { is_expected.to be_empty }
+
+ context 'if build.trace contains text' do
+ let(:text) { 'example output' }
+ before { build.trace = text }
+
+ it { is_expected.to include(text) }
+ it { expect(subject.length).to be >= text.length }
+ end
+
+ context 'if build.trace hides token' do
+ let(:token) { 'my_secret_token' }
+
+ before do
+ build.project.update_attributes(token: token)
+ build.update_attributes(trace: token)
+ end
+
+ it { is_expected.to_not include(token) }
+ end
+ end
+
+ describe :timeout do
+ subject { build.timeout }
+
+ it { is_expected.to eq(commit.project.timeout) }
+ end
+
+ describe :duration do
+ subject { build.duration }
+
+ it { is_expected.to eq(120.0) }
+
+ context 'if the building process has not started yet' do
+ before do
+ build.started_at = nil
+ build.finished_at = nil
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'if the building process has started' do
+ before do
+ build.started_at = Time.now - 1.minute
+ build.finished_at = nil
+ end
+
+ it { is_expected.to be_a(Float) }
+ it { is_expected.to be > 0.0 }
+ end
+ end
+
+ describe :options do
+ let(:options) do
+ {
+ image: "ruby:2.1",
+ services: [
+ "postgres"
+ ]
+ }
+ end
+
+ subject { build.options }
+ it { is_expected.to eq(options) }
+ end
+
+ describe :sha do
+ subject { build.sha }
+
+ it { is_expected.to eq(commit.sha) }
+ end
+
+ describe :short_sha do
+ subject { build.short_sha }
+
+ it { is_expected.to eq(commit.short_sha) }
+ end
+
+ describe :allow_git_fetch do
+ subject { build.allow_git_fetch }
+
+ it { is_expected.to eq(project.allow_git_fetch) }
+ end
+
+ describe :project do
+ subject { build.project }
+
+ it { is_expected.to eq(commit.project) }
+ end
+
+ describe :project_id do
+ subject { build.project_id }
+
+ it { is_expected.to eq(commit.project_id) }
+ end
+
+ describe :project_name do
+ subject { build.project_name }
+
+ it { is_expected.to eq(project.name) }
+ end
+
+ describe :repo_url do
+ subject { build.repo_url }
+
+ it { is_expected.to eq(project.repo_url_with_auth) }
+ end
+
+ describe :extract_coverage do
+ context 'valid content & regex' do
+ subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to eq(98.29) }
+ end
+
+ context 'valid content & bad regex' do
+ subject { build.extract_coverage('Coverage 1033 / 1051 LOC (98.29%) covered', 'very covered') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'no coverage content & regex' do
+ subject { build.extract_coverage('No coverage for today :sad:', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'multiple results in content & regex' do
+ subject { build.extract_coverage(' (98.39%) covered. (98.29%) covered', '\(\d+.\d+\%\) covered') }
+
+ it { is_expected.to eq(98.29) }
+ end
+ end
+
+ describe :variables do
+ context 'returns variables' do
+ subject { build.variables }
+
+ let(:variables) do
+ [
+ { key: :DB_NAME, value: 'postgres', public: true }
+ ]
+ end
+
+ it { is_expected.to eq(variables) }
+
+ context 'and secure variables' do
+ let(:secure_variables) do
+ [
+ { key: 'SECRET_KEY', value: 'secret_value', public: false }
+ ]
+ end
+
+ before do
+ build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ end
+
+ it { is_expected.to eq(variables + secure_variables) }
+
+ context 'and trigger variables' do
+ let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
+ let(:trigger_request) { FactoryGirl.create :ci_trigger_request_with_variables, commit: commit, trigger: trigger }
+ let(:trigger_variables) do
+ [
+ { key: :TRIGGER_KEY, value: 'TRIGGER_VALUE', public: false }
+ ]
+ end
+
+ before do
+ build.trigger_request = trigger_request
+ end
+
+ it { is_expected.to eq(variables + secure_variables + trigger_variables) }
+ end
+ end
+ end
+ end
+
+ describe :project_recipients do
+ let(:pusher_email) { 'pusher@gitlab.test' }
+ let(:user) { User.new(notification_email: pusher_email) }
+ subject { build.project_recipients }
+
+ before do
+ build.update_attributes(user: user)
+ end
+
+ it 'should return pusher_email as only recipient when no additional recipients are given' do
+ project.update_attributes(email_add_pusher: true,
+ email_recipients: '')
+ is_expected.to eq([pusher_email])
+ end
+
+ it 'should return pusher_email and additional recipients' do
+ project.update_attributes(email_add_pusher: true,
+ email_recipients: 'rec1 rec2')
+ is_expected.to eq(['rec1', 'rec2', pusher_email])
+ end
+
+ it 'should return recipients' do
+ project.update_attributes(email_add_pusher: false,
+ email_recipients: 'rec1 rec2')
+ is_expected.to eq(['rec1', 'rec2'])
+ end
+
+ it 'should return unique recipients only' do
+ project.update_attributes(email_add_pusher: true,
+ email_recipients: "rec1 rec1 #{pusher_email}")
+ is_expected.to eq(['rec1', pusher_email])
+ end
+ end
+end
diff --git a/spec/models/ci/commit_spec.rb b/spec/models/ci/commit_spec.rb
new file mode 100644
index 00000000000..acff1ddf0fc
--- /dev/null
+++ b/spec/models/ci/commit_spec.rb
@@ -0,0 +1,307 @@
+# == 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
+#
+
+require 'spec_helper'
+
+describe Ci::Commit do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+
+ it { is_expected.to belong_to(:gl_project) }
+ it { is_expected.to have_many(:builds) }
+ it { is_expected.to validate_presence_of :sha }
+
+ it { is_expected.to respond_to :git_author_name }
+ it { is_expected.to respond_to :git_author_email }
+ it { is_expected.to respond_to :short_sha }
+
+ describe :last_build do
+ subject { commit.last_build }
+ before do
+ @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
+ @second = FactoryGirl.create :ci_build, commit: commit
+ end
+
+ it { is_expected.to be_a(Ci::Build) }
+ it('returns with the most recently created build') { is_expected.to eq(@second) }
+ end
+
+ describe :retry do
+ before do
+ @first = FactoryGirl.create :ci_build, commit: commit, created_at: Date.yesterday
+ @second = FactoryGirl.create :ci_build, commit: commit
+ end
+
+ it "creates new build" do
+ expect(commit.builds.count(:all)).to eq 2
+ commit.retry
+ expect(commit.builds.count(:all)).to eq 3
+ end
+ end
+
+ describe :valid_commit_sha do
+ context 'commit.sha can not start with 00000000' do
+ before do
+ commit.sha = '0' * 40
+ commit.valid_commit_sha
+ end
+
+ it('commit errors should not be empty') { expect(commit.errors).not_to be_empty }
+ end
+ end
+
+ describe :short_sha do
+ subject { commit.short_sha }
+
+ it 'has 8 items' do
+ expect(subject.size).to eq(8)
+ end
+ it { expect(commit.sha).to start_with(subject) }
+ end
+
+ describe :stage do
+ subject { commit.stage }
+
+ before do
+ @second = FactoryGirl.create :ci_build, commit: commit, name: 'deploy', stage: 'deploy', stage_idx: 1, status: :pending
+ @first = FactoryGirl.create :ci_build, commit: commit, name: 'test', stage: 'test', stage_idx: 0, status: :pending
+ end
+
+ it 'returns first running stage' do
+ is_expected.to eq('test')
+ end
+
+ context 'first build succeeded' do
+ before do
+ @first.update_attributes(status: :success)
+ end
+
+ it 'returns last running stage' do
+ is_expected.to eq('deploy')
+ end
+ end
+
+ context 'all builds succeeded' do
+ before do
+ @first.update_attributes(status: :success)
+ @second.update_attributes(status: :success)
+ end
+
+ it 'returns nil' do
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe :create_next_builds do
+ end
+
+ describe :create_builds do
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+
+ def create_builds(trigger_request = nil)
+ commit.create_builds('master', false, nil, trigger_request)
+ end
+
+ def create_next_builds(trigger_request = nil)
+ commit.create_next_builds('master', false, nil, trigger_request)
+ end
+
+ it 'creates builds' do
+ expect(create_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+
+ expect(create_next_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(4)
+
+ expect(create_next_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(5)
+
+ expect(create_next_builds).to be_falsey
+ end
+
+ context 'for different ref' do
+ def create_develop_builds
+ commit.create_builds('develop', false, nil, nil)
+ end
+
+ it 'creates builds' do
+ expect(create_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+
+ expect(create_develop_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(4)
+ expect(commit.refs.size).to eq(2)
+ expect(commit.builds.pluck(:name).uniq.size).to eq(2)
+ end
+ end
+
+ context 'for build triggers' do
+ let(:trigger) { FactoryGirl.create :ci_trigger, project: project }
+ let(:trigger_request) { FactoryGirl.create :ci_trigger_request, commit: commit, trigger: trigger }
+
+ it 'creates builds' do
+ expect(create_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+ end
+
+ it 'rebuilds commit' do
+ expect(create_builds).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+
+ expect(create_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(4)
+ end
+
+ it 'creates next builds' do
+ expect(create_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+
+ expect(create_next_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(4)
+ end
+
+ context 'for [ci skip]' do
+ before do
+ allow(commit).to receive(:git_commit_message) { 'message [ci skip]' }
+ end
+
+ it 'rebuilds commit' do
+ expect(commit.status).to eq('skipped')
+ expect(create_builds(trigger_request)).to be_truthy
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+ expect(commit.status).to eq('pending')
+ end
+ end
+ end
+ end
+
+ describe "#finished_at" do
+ let(:commit) { FactoryGirl.create :ci_commit }
+
+ it "returns finished_at of latest build" do
+ build = FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 60
+ FactoryGirl.create :ci_build, commit: commit, finished_at: Time.now - 120
+
+ expect(commit.finished_at.to_i).to eq(build.finished_at.to_i)
+ end
+
+ it "returns nil if there is no finished build" do
+ FactoryGirl.create :ci_not_started_build, commit: commit
+
+ expect(commit.finished_at).to be_nil
+ end
+ end
+
+ describe "coverage" do
+ let(:project) { FactoryGirl.create :ci_project, coverage_regex: "/.*/" }
+ let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+
+ it "calculates average when there are two builds with coverage" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
+ expect(commit.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there are two builds with coverage and one with nil" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
+ FactoryGirl.create :ci_build, commit: commit
+ expect(commit.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there are two builds with coverage and one is retried" do
+ FactoryGirl.create :ci_build, name: "rspec", coverage: 30, commit: commit
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 30, commit: commit
+ FactoryGirl.create :ci_build, name: "rubocop", coverage: 40, commit: commit
+ expect(commit.coverage).to eq("35.00")
+ end
+
+ it "calculates average when there is one build without coverage" do
+ FactoryGirl.create :ci_build, commit: commit
+ expect(commit.coverage).to be_nil
+ end
+ end
+
+ describe :should_create_next_builds? do
+ before do
+ @build1 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: false, status: :success
+ @build2 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'develop', tag: false, status: :failed
+ @build3 = FactoryGirl.create :ci_build, commit: commit, name: 'build1', ref: 'master', tag: true, status: :failed
+ @build4 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :success
+ end
+
+ context 'for success' do
+ it 'to create if all succeeded' do
+ expect(commit.should_create_next_builds?(@build4)).to be_truthy
+ end
+ end
+
+ context 'for failed' do
+ before do
+ @build4.update_attributes(status: :failed)
+ end
+
+ it 'to not create' do
+ expect(commit.should_create_next_builds?(@build4)).to be_falsey
+ end
+
+ context 'and ignore failures for current' do
+ before do
+ @build4.update_attributes(allow_failure: true)
+ end
+
+ it 'to create' do
+ expect(commit.should_create_next_builds?(@build4)).to be_truthy
+ end
+ end
+ end
+
+ context 'for running' do
+ before do
+ @build4.update_attributes(status: :running)
+ end
+
+ it 'to not create' do
+ expect(commit.should_create_next_builds?(@build4)).to be_falsey
+ end
+ end
+
+ context 'for retried' do
+ before do
+ @build5 = FactoryGirl.create :ci_build, commit: commit, name: 'build4', ref: 'master', tag: false, status: :failed
+ end
+
+ it 'to not create' do
+ expect(commit.should_create_next_builds?(@build4)).to be_falsey
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/hip_chat_message_spec.rb b/spec/models/ci/project_services/hip_chat_message_spec.rb
new file mode 100644
index 00000000000..e23d6ae2c28
--- /dev/null
+++ b/spec/models/ci/project_services/hip_chat_message_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Ci::HipChatMessage do
+ subject { Ci::HipChatMessage.new(build) }
+
+ let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) }
+
+ let(:build) do
+ commit.builds.first
+ end
+
+ context 'when all matrix builds succeed' do
+ it 'returns a successful message' do
+ commit.create_builds('master', false, nil)
+ commit.builds.update_all(status: "success")
+ commit.reload
+
+ expect(subject.status_color).to eq 'green'
+ expect(subject.notify?).to be_falsey
+ expect(subject.to_s).to match(/Commit #\d+/)
+ expect(subject.to_s).to match(/Successful in \d+ second\(s\)\./)
+ end
+ end
+
+ context 'when at least one matrix build fails' do
+ it 'returns a failure message' do
+ commit.create_builds('master', false, nil)
+ first_build = commit.builds.first
+ second_build = commit.builds.last
+ first_build.update(status: "success")
+ second_build.update(status: "failed")
+
+ expect(subject.status_color).to eq 'red'
+ expect(subject.notify?).to be_truthy
+ expect(subject.to_s).to match(/Commit #\d+/)
+ expect(subject.to_s).to match(/Failed in \d+ second\(s\)\./)
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/hip_chat_service_spec.rb b/spec/models/ci/project_services/hip_chat_service_spec.rb
new file mode 100644
index 00000000000..d9ccc855edf
--- /dev/null
+++ b/spec/models/ci/project_services/hip_chat_service_spec.rb
@@ -0,0 +1,73 @@
+# == 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
+#
+
+
+require 'spec_helper'
+
+describe Ci::HipChatService do
+
+ describe "Validations" do
+
+ context "active" do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :hipchat_room }
+ it { is_expected.to validate_presence_of :hipchat_token }
+
+ end
+ end
+
+ describe "Execute" do
+
+ let(:service) { Ci::HipChatService.new }
+ let(:commit) { FactoryGirl.create :ci_commit }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
+ let(:api_url) { 'https://api.hipchat.com/v2/room/123/notification?auth_token=a1b2c3d4e5f6' }
+
+ before do
+ allow(service).to receive_messages(
+ project: commit.project,
+ project_id: commit.project_id,
+ notify_only_broken_builds: false,
+ hipchat_room: 123,
+ hipchat_token: 'a1b2c3d4e5f6'
+ )
+
+ WebMock.stub_request(:post, api_url)
+ end
+
+
+ it "should call the HipChat API" do
+ service.execute(build)
+ Ci::HipChatNotifierWorker.drain
+
+ expect( WebMock ).to have_requested(:post, api_url).once
+ end
+
+ it "calls the worker with expected arguments" do
+ expect( Ci::HipChatNotifierWorker ).to receive(:perform_async) \
+ .with(an_instance_of(String), hash_including(
+ token: 'a1b2c3d4e5f6',
+ room: 123,
+ server: 'https://api.hipchat.com',
+ color: 'red',
+ notify: true
+ ))
+
+ service.execute(build)
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/mail_service_spec.rb b/spec/models/ci/project_services/mail_service_spec.rb
new file mode 100644
index 00000000000..04e870dce7f
--- /dev/null
+++ b/spec/models/ci/project_services/mail_service_spec.rb
@@ -0,0 +1,191 @@
+# == 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
+#
+
+require 'spec_helper'
+
+describe Ci::MailService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe "Validations" do
+ context "active" do
+ before do
+ subject.active = true
+ end
+ end
+ end
+
+ describe 'Sends email for' do
+ let(:mail) { Ci::MailService.new }
+ let(:user) { User.new(notification_email: 'git@example.com')}
+
+ describe 'failed build' do
+ let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ should_email("git@example.com")
+ mail.execute(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).to receive(:build_fail_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
+ end
+ end
+
+ describe 'successfull build' do
+ let(:project) { FactoryGirl.create(:ci_project, email_add_pusher: true, email_only_broken_builds: false) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ should_email("git@example.com")
+ mail.execute(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).to receive(:build_success_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ end
+ end
+
+ describe 'successfull build and project has email_recipients' do
+ let(:project) do
+ FactoryGirl.create(:ci_project,
+ email_add_pusher: true,
+ email_only_broken_builds: false,
+ email_recipients: "jeroen@example.com")
+ end
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ should_email("git@example.com")
+ should_email("jeroen@example.com")
+ mail.execute(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).to receive(:build_success_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ end
+ end
+
+ describe 'successful build and notify only broken builds' do
+ let(:project) do
+ FactoryGirl.create(:ci_project,
+ email_add_pusher: true,
+ email_only_broken_builds: true,
+ email_recipients: "jeroen@example.com")
+ end
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ should_email(commit.git_author_email)
+ should_email("jeroen@example.com")
+ mail.execute(build) if mail.can_execute?(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ end
+ end
+
+ describe 'successful build and can test service' do
+ let(:project) do
+ FactoryGirl.create(:ci_project,
+ email_add_pusher: true,
+ email_only_broken_builds: false,
+ email_recipients: "jeroen@example.com")
+ end
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :success, commit: commit, user: user) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ build
+ end
+
+ it do
+ expect(mail.can_test?).to eq(true)
+ end
+ end
+
+ describe 'retried build should not receive email' do
+ let(:project) do
+ FactoryGirl.create(:ci_project,
+ email_add_pusher: true,
+ email_only_broken_builds: true,
+ email_recipients: "jeroen@example.com")
+ end
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+ let(:build) { FactoryGirl.create(:ci_build, status: :failed, commit: commit, user: user) }
+
+ before do
+ allow(mail).to receive_messages(
+ project: project
+ )
+ end
+
+ it do
+ Ci::Build.retry(build)
+ should_email(commit.git_author_email)
+ should_email("jeroen@example.com")
+ mail.execute(build) if mail.can_execute?(build)
+ end
+
+ def should_email(email)
+ expect(Ci::Notify).not_to receive(:build_success_email).with(build.id, email)
+ expect(Ci::Notify).not_to receive(:build_fail_email).with(build.id, email)
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/slack_message_spec.rb b/spec/models/ci/project_services/slack_message_spec.rb
new file mode 100644
index 00000000000..8adda6c86cc
--- /dev/null
+++ b/spec/models/ci/project_services/slack_message_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe Ci::SlackMessage do
+ subject { Ci::SlackMessage.new(commit) }
+
+ let(:commit) { FactoryGirl.create(:ci_commit_with_two_jobs) }
+
+ context 'when all matrix builds succeeded' do
+ let(:color) { 'good' }
+
+ it 'returns a message with success' do
+ commit.create_builds('master', false, nil)
+ commit.builds.update_all(status: "success")
+ commit.reload
+
+ expect(subject.color).to eq(color)
+ expect(subject.fallback).to include('Commit')
+ expect(subject.fallback).to include("\##{commit.id}")
+ expect(subject.fallback).to include('succeeded')
+ expect(subject.attachments.first[:fields]).to be_empty
+ end
+ end
+
+ context 'when one of matrix builds failed' do
+ let(:color) { 'danger' }
+
+ it 'returns a message with information about failed build' do
+ commit.create_builds('master', false, nil)
+ first_build = commit.builds.first
+ second_build = commit.builds.last
+ first_build.update(status: "success")
+ second_build.update(status: "failed")
+
+ expect(subject.color).to eq(color)
+ expect(subject.fallback).to include('Commit')
+ expect(subject.fallback).to include("\##{commit.id}")
+ expect(subject.fallback).to include('failed')
+ expect(subject.attachments.first[:fields].size).to eq(1)
+ expect(subject.attachments.first[:fields].first[:title]).to eq(second_build.name)
+ expect(subject.attachments.first[:fields].first[:value]).to include("\##{second_build.id}")
+ end
+ end
+end
diff --git a/spec/models/ci/project_services/slack_service_spec.rb b/spec/models/ci/project_services/slack_service_spec.rb
new file mode 100644
index 00000000000..1ac7dfe568d
--- /dev/null
+++ b/spec/models/ci/project_services/slack_service_spec.rb
@@ -0,0 +1,57 @@
+# == 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
+#
+
+require 'spec_helper'
+
+describe Ci::SlackService do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe "Validations" do
+ context "active" do
+ before do
+ subject.active = true
+ end
+
+ it { is_expected.to validate_presence_of :webhook }
+ end
+ end
+
+ describe "Execute" do
+ let(:slack) { Ci::SlackService.new }
+ let(:commit) { FactoryGirl.create :ci_commit }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit, status: 'failed' }
+ let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
+ let(:notify_only_broken_builds) { false }
+
+ before do
+ allow(slack).to receive_messages(
+ project: commit.project,
+ project_id: commit.project_id,
+ webhook: webhook_url,
+ notify_only_broken_builds: notify_only_broken_builds
+ )
+
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it "should call Slack API" do
+ slack.execute(build)
+ Ci::SlackNotifierWorker.drain
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+end
diff --git a/spec/models/ci/project_spec.rb b/spec/models/ci/project_spec.rb
new file mode 100644
index 00000000000..dec4720a711
--- /dev/null
+++ b/spec/models/ci/project_spec.rb
@@ -0,0 +1,263 @@
+# == 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
+#
+
+require 'spec_helper'
+
+describe Ci::Project do
+ let(:gl_project) { FactoryGirl.create :empty_project }
+ let(:project) { FactoryGirl.create :ci_project, gl_project: gl_project }
+ subject { project }
+
+ it { is_expected.to have_many(:runner_projects) }
+ it { is_expected.to have_many(:runners) }
+ it { is_expected.to have_many(:web_hooks) }
+ it { is_expected.to have_many(:events) }
+ it { is_expected.to have_many(:variables) }
+ it { is_expected.to have_many(:triggers) }
+ it { is_expected.to have_many(:services) }
+
+ it { is_expected.to validate_presence_of :timeout }
+ it { is_expected.to validate_presence_of :gitlab_id }
+
+ describe 'before_validation' do
+ it 'should set an random token if none provided' do
+ project = FactoryGirl.create :ci_project_without_token
+ expect(project.token).not_to eq("")
+ end
+
+ it 'should not set an random toke if one provided' do
+ project = FactoryGirl.create :ci_project
+ expect(project.token).to eq("iPWx6WM4lhHNedGfBpPJNP")
+ end
+ end
+
+ describe :name_with_namespace do
+ subject { project.name_with_namespace }
+
+ it { is_expected.to eq(project.name) }
+ it { is_expected.to eq(gl_project.name_with_namespace) }
+ end
+
+ describe :path_with_namespace do
+ subject { project.path_with_namespace }
+
+ it { is_expected.to eq(project.path) }
+ it { is_expected.to eq(gl_project.path_with_namespace) }
+ end
+
+ describe :path_with_namespace do
+ subject { project.web_url }
+
+ it { is_expected.to eq(gl_project.web_url) }
+ end
+
+ describe :web_url do
+ subject { project.web_url }
+
+ it { is_expected.to eq(project.gitlab_url) }
+ it { is_expected.to eq(gl_project.web_url) }
+ end
+
+ describe :http_url_to_repo do
+ subject { project.http_url_to_repo }
+
+ it { is_expected.to eq(gl_project.http_url_to_repo) }
+ end
+
+ describe :ssh_url_to_repo do
+ subject { project.ssh_url_to_repo }
+
+ it { is_expected.to eq(gl_project.ssh_url_to_repo) }
+ end
+
+ describe :commits do
+ subject { project.commits }
+
+ before do
+ FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project
+ end
+
+ it { is_expected.to eq(gl_project.ci_commits) }
+ end
+
+ describe :builds do
+ subject { project.builds }
+
+ before do
+ commit = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: gl_project
+ FactoryGirl.create :ci_build, commit: commit
+ end
+
+ it { is_expected.to eq(gl_project.ci_builds) }
+ end
+
+ describe "ordered_by_last_commit_date" do
+ it "returns ordered projects" do
+ newest_project = FactoryGirl.create :empty_project
+ newest_ci_project = newest_project.ensure_gitlab_ci_project
+ oldest_project = FactoryGirl.create :empty_project
+ oldest_ci_project = oldest_project.ensure_gitlab_ci_project
+ project_without_commits = FactoryGirl.create :empty_project
+ ci_project_without_commits = project_without_commits.ensure_gitlab_ci_project
+
+ FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: newest_project
+ FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: oldest_project
+
+ expect(Ci::Project.ordered_by_last_commit_date).to eq([newest_ci_project, oldest_ci_project, ci_project_without_commits])
+ end
+ end
+
+ describe 'ordered commits' do
+ let(:project) { FactoryGirl.create :empty_project }
+
+ it 'returns ordered list of commits' do
+ commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
+ commit2 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
+ expect(project.ci_commits).to eq([commit2, commit1])
+ end
+
+ it 'returns commits ordered by committed_at and id, with nulls last' do
+ commit1 = FactoryGirl.create :ci_commit, committed_at: 1.hour.ago, gl_project: project
+ commit2 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
+ commit3 = FactoryGirl.create :ci_commit, committed_at: 2.hour.ago, gl_project: project
+ commit4 = FactoryGirl.create :ci_commit, committed_at: nil, gl_project: project
+ expect(project.ci_commits).to eq([commit2, commit4, commit3, commit1])
+ end
+ end
+
+ context :valid_project do
+ let(:commit) { FactoryGirl.create(:ci_commit) }
+
+ context :project_with_commit_and_builds do
+ let(:project) { commit.project }
+
+ before do
+ FactoryGirl.create(:ci_build, commit: commit)
+ end
+
+ it { expect(project.status).to eq('pending') }
+ it { expect(project.last_commit).to be_kind_of(Ci::Commit) }
+ it { expect(project.human_status).to eq('pending') }
+ end
+ end
+
+ describe '#email_notification?' do
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ expect(project.email_notification?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: 'test tesft'
+ expect(project.email_notification?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: false, email_recipients: ''
+ expect(project.email_notification?).to eq(false)
+ end
+ end
+
+ describe '#broken_or_success?' do
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ allow(project).to receive(:broken?).and_return(true)
+ allow(project).to receive(:success?).and_return(true)
+ expect(project.broken_or_success?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ allow(project).to receive(:broken?).and_return(true)
+ allow(project).to receive(:success?).and_return(false)
+ expect(project.broken_or_success?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ allow(project).to receive(:broken?).and_return(false)
+ allow(project).to receive(:success?).and_return(true)
+ expect(project.broken_or_success?).to eq(true)
+ end
+
+ it do
+ project = FactoryGirl.create :ci_project, email_add_pusher: true
+ allow(project).to receive(:broken?).and_return(false)
+ allow(project).to receive(:success?).and_return(false)
+ expect(project.broken_or_success?).to eq(false)
+ end
+ end
+
+ describe 'Project.parse' do
+ let(:project) { FactoryGirl.create :project }
+
+ subject { Ci::Project.parse(project) }
+
+ it { is_expected.to be_valid }
+ it { is_expected.to be_kind_of(Ci::Project) }
+ it { expect(subject.name).to eq(project.name_with_namespace) }
+ it { expect(subject.gitlab_id).to eq(project.id) }
+ it { expect(subject.gitlab_url).to eq(project.web_url) }
+ end
+
+ describe :repo_url_with_auth do
+ let(:project) { FactoryGirl.create :ci_project }
+ subject { project.repo_url_with_auth }
+
+ it { is_expected.to be_a(String) }
+ it { is_expected.to end_with(".git") }
+ it { is_expected.to start_with(project.gitlab_url[0..6]) }
+ it { is_expected.to include(project.token) }
+ it { is_expected.to include('gitlab-ci-token') }
+ it { is_expected.to include(project.gitlab_url[7..-1]) }
+ end
+
+ describe :any_runners do
+ it "there are no runners available" do
+ project = FactoryGirl.create(:ci_project)
+ expect(project.any_runners?).to be_falsey
+ end
+
+ it "there is a specific runner" do
+ project = FactoryGirl.create(:ci_project)
+ project.runners << FactoryGirl.create(:ci_specific_runner)
+ expect(project.any_runners?).to be_truthy
+ end
+
+ it "there is a shared runner" do
+ project = FactoryGirl.create(:ci_project, shared_runners_enabled: true)
+ FactoryGirl.create(:ci_shared_runner)
+ expect(project.any_runners?).to be_truthy
+ end
+
+ it "there is a shared runner, but they are prohibited to use" do
+ project = FactoryGirl.create(:ci_project)
+ FactoryGirl.create(:ci_shared_runner)
+ expect(project.any_runners?).to be_falsey
+ end
+ end
+end
diff --git a/spec/models/ci/runner_project_spec.rb b/spec/models/ci/runner_project_spec.rb
new file mode 100644
index 00000000000..0218d484130
--- /dev/null
+++ b/spec/models/ci/runner_project_spec.rb
@@ -0,0 +1,16 @@
+# == 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
+#
+
+require 'spec_helper'
+
+describe Ci::RunnerProject do
+ pending "add some examples to (or delete) #{__FILE__}"
+end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
new file mode 100644
index 00000000000..757593a7ab8
--- /dev/null
+++ b/spec/models/ci/runner_spec.rb
@@ -0,0 +1,70 @@
+# == 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)
+#
+
+require 'spec_helper'
+
+describe Ci::Runner do
+ describe '#display_name' do
+ it 'should return the description if it has a value' do
+ runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
+ expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448'
+ end
+
+ it 'should return the token if it does not have a description' do
+ runner = FactoryGirl.create(:ci_runner)
+ expect(runner.display_name).to eq runner.description
+ end
+
+ it 'should return the token if the description is an empty string' do
+ runner = FactoryGirl.build(:ci_runner, description: '')
+ expect(runner.display_name).to eq runner.token
+ end
+ end
+
+ describe :assign_to do
+ let!(:project) { FactoryGirl.create :ci_project }
+ let!(:shared_runner) { FactoryGirl.create(:ci_shared_runner) }
+
+ before { shared_runner.assign_to(project) }
+
+ it { expect(shared_runner).to be_specific }
+ it { expect(shared_runner.projects).to eq([project]) }
+ it { expect(shared_runner.only_for?(project)).to be_truthy }
+ end
+
+ describe "belongs_to_one_project?" do
+ it "returns false if there are two projects runner assigned to" do
+ runner = FactoryGirl.create(:ci_specific_runner)
+ project = FactoryGirl.create(:ci_project)
+ project1 = FactoryGirl.create(:ci_project)
+ project.runners << runner
+ project1.runners << runner
+
+ expect(runner.belongs_to_one_project?).to be_falsey
+ end
+
+ it "returns true" do
+ runner = FactoryGirl.create(:ci_specific_runner)
+ project = FactoryGirl.create(:ci_project)
+ project.runners << runner
+
+ expect(runner.belongs_to_one_project?).to be_truthy
+ end
+ end
+end
diff --git a/spec/models/ci/service_spec.rb b/spec/models/ci/service_spec.rb
new file mode 100644
index 00000000000..2df70e88888
--- /dev/null
+++ b/spec/models/ci/service_spec.rb
@@ -0,0 +1,48 @@
+# == 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
+#
+
+require 'spec_helper'
+
+describe Ci::Service do
+
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe "Mass assignment" do
+ end
+
+ describe "Test Button" do
+ before do
+ @service = Ci::Service.new
+ end
+
+ describe "Testable" do
+ let(:commit) { FactoryGirl.create :ci_commit }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit }
+
+ before do
+ allow(@service).to receive_messages(
+ project: commit.project
+ )
+ build
+ @testable = @service.can_test?
+ end
+
+ describe :can_test do
+ it { expect(@testable).to eq(true) }
+ end
+ end
+ end
+end
diff --git a/spec/models/ci/trigger_spec.rb b/spec/models/ci/trigger_spec.rb
new file mode 100644
index 00000000000..19c14ef2da2
--- /dev/null
+++ b/spec/models/ci/trigger_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Ci::Trigger do
+ let(:project) { FactoryGirl.create :ci_project }
+
+ describe 'before_validation' do
+ it 'should set an random token if none provided' do
+ trigger = FactoryGirl.create :ci_trigger_without_token, project: project
+ expect(trigger.token).not_to be_nil
+ end
+
+ it 'should not set an random token if one provided' do
+ trigger = FactoryGirl.create :ci_trigger, project: project
+ expect(trigger.token).to eq('token')
+ end
+ end
+end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
new file mode 100644
index 00000000000..d034a6c7b9f
--- /dev/null
+++ b/spec/models/ci/variable_spec.rb
@@ -0,0 +1,45 @@
+# == 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)
+#
+
+require 'spec_helper'
+
+describe Ci::Variable do
+ subject { Ci::Variable.new }
+
+ let(:secret_value) { 'secret' }
+
+ before :each do
+ subject.value = secret_value
+ end
+
+ describe :value do
+ it 'stores the encrypted value' do
+ expect(subject.encrypted_value).not_to be_nil
+ end
+
+ it 'stores an iv for value' do
+ expect(subject.encrypted_value_iv).not_to be_nil
+ end
+
+ it 'stores a salt for value' do
+ expect(subject.encrypted_value_salt).not_to be_nil
+ end
+
+ it 'fails to decrypt if iv is incorrect' do
+ subject.encrypted_value_iv = nil
+ subject.instance_variable_set(:@value, nil)
+ expect { subject.value }.
+ to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
+ end
+ end
+end
diff --git a/spec/models/ci/web_hook_spec.rb b/spec/models/ci/web_hook_spec.rb
new file mode 100644
index 00000000000..bf9481ab81d
--- /dev/null
+++ b/spec/models/ci/web_hook_spec.rb
@@ -0,0 +1,63 @@
+# == 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
+#
+
+require 'spec_helper'
+
+describe Ci::WebHook do
+ describe "Associations" do
+ it { is_expected.to belong_to :project }
+ end
+
+ describe "Validations" do
+ it { is_expected.to validate_presence_of(:url) }
+
+ context "url format" do
+ it { is_expected.to allow_value("http://example.com").for(:url) }
+ it { is_expected.to allow_value("https://excample.com").for(:url) }
+ it { is_expected.to allow_value("http://test.com/api").for(:url) }
+ it { is_expected.to allow_value("http://test.com/api?key=abc").for(:url) }
+ it { is_expected.to allow_value("http://test.com/api?key=abc&type=def").for(:url) }
+
+ it { is_expected.not_to allow_value("example.com").for(:url) }
+ it { is_expected.not_to allow_value("ftp://example.com").for(:url) }
+ it { is_expected.not_to allow_value("herp-and-derp").for(:url) }
+ end
+ end
+
+ describe "execute" do
+ before(:each) do
+ @web_hook = FactoryGirl.create(:ci_web_hook)
+ @project = @web_hook.project
+ @data = { before: 'oldrev', after: 'newrev', ref: 'ref' }
+
+ WebMock.stub_request(:post, @web_hook.url)
+ end
+
+ it "POSTs to the web hook URL" do
+ @web_hook.execute(@data)
+ expect(WebMock).to have_requested(:post, @web_hook.url).once
+ end
+
+ it "POSTs the data as JSON" do
+ json = @data.to_json
+
+ @web_hook.execute(@data)
+ expect(WebMock).to have_requested(:post, @web_hook.url).with(body: json).once
+ end
+
+ it "catches exceptions" do
+ expect(Ci::WebHook).to receive(:post).and_raise("Some HTTP Post error")
+
+ expect{ @web_hook.execute(@data) }.
+ to raise_error(RuntimeError, 'Some HTTP Post error')
+ end
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index b6d80451d2e..8f706f8934b 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe Issue, "Issuable" do
let(:issue) { create(:issue) }
+ let(:user) { create(:user) }
describe "Associations" do
it { is_expected.to belong_to(:project) }
@@ -66,4 +67,19 @@ describe Issue, "Issuable" do
expect(issue.new?).to be_falsey
end
end
+
+
+ describe "#to_hook_data" do
+ let(:hook_data) { issue.to_hook_data(user) }
+
+ it "returns correct hook data" do
+ expect(hook_data[:object_kind]).to eq("issue")
+ expect(hook_data[:user]).to eq(user.hook_attrs)
+ expect(hook_data[:repository][:name]).to eq(issue.project.name)
+ expect(hook_data[:repository][:url]).to eq(issue.project.url_to_repo)
+ expect(hook_data[:repository][:description]).to eq(issue.project.description)
+ expect(hook_data[:repository][:homepage]).to eq(issue.project.web_url)
+ expect(hook_data[:object_attributes]).to eq(issue.hook_attrs)
+ end
+ end
end
diff --git a/spec/models/hooks/project_hook_spec.rb b/spec/models/hooks/project_hook_spec.rb
index dae7e399cfb..a2dc66fce3e 100644
--- a/spec/models/hooks/project_hook_spec.rb
+++ b/spec/models/hooks/project_hook_spec.rb
@@ -22,7 +22,7 @@ describe ProjectHook do
describe '.push_hooks' do
it 'should return hooks for push events only' do
hook = create(:project_hook, push_events: true)
- hook2 = create(:project_hook, push_events: false)
+ create(:project_hook, push_events: false)
expect(ProjectHook.push_hooks).to eq([hook])
end
end
@@ -30,7 +30,7 @@ describe ProjectHook do
describe '.tag_push_hooks' do
it 'should return hooks for tag push events only' do
hook = create(:project_hook, tag_push_events: true)
- hook2 = create(:project_hook, tag_push_events: false)
+ create(:project_hook, tag_push_events: false)
expect(ProjectHook.tag_push_hooks).to eq([hook])
end
end
diff --git a/spec/models/hooks/service_hook_spec.rb b/spec/models/hooks/service_hook_spec.rb
index 4c8b8910ae7..16641c12124 100644
--- a/spec/models/hooks/service_hook_spec.rb
+++ b/spec/models/hooks/service_hook_spec.rb
@@ -39,8 +39,6 @@ describe ServiceHook do
end
it "POSTs the data as JSON" do
- json = @data.to_json
-
@service_hook.execute(@data)
expect(WebMock).to have_requested(:post, @service_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Service Hook' }
diff --git a/spec/models/hooks/web_hook_spec.rb b/spec/models/hooks/web_hook_spec.rb
index 23f30881d99..2fdc49f02ee 100644
--- a/spec/models/hooks/web_hook_spec.rb
+++ b/spec/models/hooks/web_hook_spec.rb
@@ -60,8 +60,6 @@ describe ProjectHook do
end
it "POSTs the data as JSON" do
- json = @data.to_json
-
@project_hook.execute(@data, 'push_hooks')
expect(WebMock).to have_requested(:post, @project_hook.url).with(
headers: { 'Content-Type'=>'application/json', 'X-Gitlab-Event'=>'Push Hook' }
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 9bac451c28c..cf336d82957 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -2,19 +2,20 @@
#
# Table name: issues
#
-# id :integer not null, primary key
-# title :string(255)
-# assignee_id :integer
-# author_id :integer
-# project_id :integer
-# created_at :datetime
-# updated_at :datetime
-# position :integer default(0)
-# branch_name :string(255)
-# description :text
-# milestone_id :integer
-# state :string(255)
-# iid :integer
+# id :integer not null, primary key
+# title :string(255)
+# assignee_id :integer
+# author_id :integer
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# position :integer default(0)
+# branch_name :string(255)
+# description :text
+# milestone_id :integer
+# state :string(255)
+# iid :integer
+# updated_by_id :integer
#
require 'spec_helper'
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index b91687bc09f..17a49013d25 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -19,6 +19,7 @@
# description :text
# position :integer default(0)
# locked_at :datetime
+# updated_by_id :integer
#
require 'spec_helper'
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 36352e1ecce..c88d5349663 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -111,8 +111,8 @@ describe Milestone do
describe :is_empty? do
before do
- issue = create :closed_issue, milestone: milestone
- merge_request = create :merge_request, milestone: milestone
+ create :closed_issue, milestone: milestone
+ create :merge_request, milestone: milestone
end
it 'Should return total count of issues and merge requests assigned to milestone' do
@@ -125,7 +125,7 @@ describe Milestone do
milestone = create :milestone
create :closed_issue, milestone: milestone
- issue = create :issue
+ create :issue
end
it 'should be true if milestone active and all nested issues closed' do
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index 331505a01b3..3a0b194ba1e 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -15,6 +15,7 @@
# noteable_id :integer
# system :boolean default(FALSE), not null
# st_diff :text
+# updated_by_id :integer
#
require 'spec_helper'
diff --git a/spec/models/project_services/buildkite_service_spec.rb b/spec/models/project_services/buildkite_service_spec.rb
index 9445d96c337..230807ea672 100644
--- a/spec/models/project_services/buildkite_service_spec.rb
+++ b/spec/models/project_services/buildkite_service_spec.rb
@@ -63,19 +63,5 @@ describe BuildkiteService do
)
end
end
-
- describe :builds_page do
- it 'returns the correct path to the builds page' do
- expect(@service.builds_path).to eq(
- 'https://buildkite.com/account-name/example-project/builds?branch=default-brancho'
- )
- end
- end
-
- describe :status_img_path do
- it 'returns the correct path to the status image' do
- expect(@service.status_img_path).to eq('https://badge.buildkite.com/secret-sauce-status-token.svg')
- end
- end
end
end
diff --git a/spec/models/project_services/drone_ci_service_spec.rb b/spec/models/project_services/drone_ci_service_spec.rb
new file mode 100644
index 00000000000..e9967f5fe0b
--- /dev/null
+++ b/spec/models/project_services/drone_ci_service_spec.rb
@@ -0,0 +1,104 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+# push_events :boolean default(TRUE)
+# issues_events :boolean default(TRUE)
+# merge_requests_events :boolean default(TRUE)
+# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
+#
+
+require 'spec_helper'
+
+describe DroneCiService do
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ it { is_expected.to have_one(:service_hook) }
+ end
+
+ describe 'validations' do
+ context 'active' do
+ before { allow(subject).to receive(:activated?).and_return(true) }
+
+ it { is_expected.to validate_presence_of(:token) }
+ it { is_expected.to validate_presence_of(:drone_url) }
+ it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) }
+ it { is_expected.to allow_value('http://ci.example.com').for(:drone_url) }
+ it { is_expected.not_to allow_value('this is not url').for(:drone_url) }
+ it { is_expected.not_to allow_value('http//noturl').for(:drone_url) }
+ it { is_expected.not_to allow_value('ftp://ci.example.com').for(:drone_url) }
+ end
+
+ context 'inactive' do
+ before { allow(subject).to receive(:activated?).and_return(false) }
+
+ it { is_expected.not_to validate_presence_of(:token) }
+ it { is_expected.not_to validate_presence_of(:drone_url) }
+ it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) }
+ it { is_expected.to allow_value('http://drone.example.com').for(:drone_url) }
+ it { is_expected.to allow_value('ftp://drone.example.com').for(:drone_url) }
+ end
+ end
+
+ shared_context :drone_ci_service do
+ let(:drone) { DroneCiService.new }
+ let(:project) { create(:project, name: 'project') }
+ let(:path) { "#{project.namespace.path}/#{project.path}" }
+ let(:drone_url) { 'http://drone.example.com' }
+ let(:sha) { '2ab7834c' }
+ let(:branch) { 'dev' }
+ let(:token) { 'secret' }
+ let(:iid) { rand(1..9999) }
+
+ before(:each) do
+ allow(drone).to receive_messages(
+ project_id: project.id,
+ project: project,
+ active: true,
+ drone_url: drone_url,
+ token: token
+ )
+ end
+ end
+
+ describe "service page/path methods" do
+ include_context :drone_ci_service
+
+ # URL's
+ let(:commit_page) { "#{drone_url}/gitlab/#{path}/redirect/commits/#{sha}?branch=#{branch}" }
+ let(:merge_request_page) { "#{drone_url}/gitlab/#{path}/redirect/pulls/#{iid}" }
+ let(:commit_status_path) { "#{drone_url}/gitlab/#{path}/commits/#{sha}?branch=#{branch}&access_token=#{token}" }
+ let(:merge_request_status_path) { "#{drone_url}/gitlab/#{path}/pulls/#{iid}?access_token=#{token}" }
+
+ it { expect(drone.build_page(sha, branch)).to eq(commit_page) }
+ it { expect(drone.commit_page(sha, branch)).to eq(commit_page) }
+ it { expect(drone.merge_request_page(iid, sha, branch)).to eq(merge_request_page) }
+ it { expect(drone.commit_status_path(sha, branch)).to eq(commit_status_path) }
+ it { expect(drone.merge_request_status_path(iid, sha, branch)).to eq(merge_request_status_path) }
+ end
+
+ describe "execute" do
+ include_context :drone_ci_service
+
+ let(:user) { create(:user, username: 'username') }
+ let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
+
+ it do
+ service_hook = double
+ expect(service_hook).to receive(:execute)
+ expect(drone).to receive(:service_hook).and_return(service_hook)
+
+ drone.execute(push_sample_data)
+ end
+ end
+end
diff --git a/spec/models/project_services/gitlab_ci_service_spec.rb b/spec/models/project_services/gitlab_ci_service_spec.rb
index a14384c87b4..842089ebe0d 100644
--- a/spec/models/project_services/gitlab_ci_service_spec.rb
+++ b/spec/models/project_services/gitlab_ci_service_spec.rb
@@ -26,51 +26,20 @@ describe GitlabCiService do
it { is_expected.to have_one(:service_hook) }
end
- describe 'validations' do
- context 'active' do
- before { allow(subject).to receive(:activated?).and_return(true) }
-
- it { is_expected.to validate_presence_of(:token) }
- it { is_expected.to validate_presence_of(:project_url) }
- it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) }
- it { is_expected.to allow_value('http://ci.example.com/project/1').for(:project_url) }
- it { is_expected.not_to allow_value('token with spaces').for(:token) }
- it { is_expected.not_to allow_value('token/with%spaces').for(:token) }
- it { is_expected.not_to allow_value('this is not url').for(:project_url) }
- it { is_expected.not_to allow_value('http//noturl').for(:project_url) }
- it { is_expected.not_to allow_value('ftp://ci.example.com/projects/3').for(:project_url) }
- end
-
- context 'inactive' do
- before { allow(subject).to receive(:activated?).and_return(false) }
-
- it { is_expected.not_to validate_presence_of(:token) }
- it { is_expected.not_to validate_presence_of(:project_url) }
- it { is_expected.to allow_value('ewf9843kdnfdfs89234n').for(:token) }
- it { is_expected.to allow_value('http://ci.example.com/project/1').for(:project_url) }
- it { is_expected.to allow_value('token with spaces').for(:token) }
- it { is_expected.to allow_value('ftp://ci.example.com/projects/3').for(:project_url) }
- end
- end
-
describe 'commits methods' do
before do
+ @ci_project = create(:ci_project)
@service = GitlabCiService.new
allow(@service).to receive_messages(
service_hook: true,
project_url: 'http://ci.gitlab.org/projects/2',
- token: 'verySecret'
+ token: 'verySecret',
+ project: @ci_project.gl_project
)
end
- describe :commit_status_path do
- it { expect(@service.commit_status_path("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c/status.json?token=verySecret")}
- it { expect(@service.commit_status_path("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232/status.json?token=verySecret")}
- end
-
describe :build_page do
- it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/2ab7834c")}
- it { expect(@service.build_page("issue#2", 'master')).to eq("http://ci.gitlab.org/projects/2/refs/master/commits/issue%232")}
+ it { expect(@service.build_page("2ab7834c", 'master')).to eq("http://localhost/#{@ci_project.gl_project.path_with_namespace}/commit/2ab7834c/ci")}
end
describe "execute" do
@@ -78,35 +47,11 @@ describe GitlabCiService do
let(:project) { create(:project, name: 'project') }
let(:push_sample_data) { Gitlab::PushDataBuilder.build_sample(project, user) }
- it "calls ci_yaml_file" do
- service_hook = double
- expect(service_hook).to receive(:execute)
- expect(@service).to receive(:service_hook).and_return(service_hook)
- expect(@service).to receive(:ci_yaml_file).with(push_sample_data[:checkout_sha])
+ it "calls CreateCommitService" do
+ expect_any_instance_of(Ci::CreateCommitService).to receive(:execute).with(@ci_project, user, push_sample_data)
@service.execute(push_sample_data)
end
end
end
-
- describe "Fork registration" do
- before do
- @old_project = create(:empty_project)
- @project = create(:empty_project)
- @user = create(:user)
-
- @service = GitlabCiService.new
- allow(@service).to receive_messages(
- service_hook: true,
- project_url: 'http://ci.gitlab.org/projects/2',
- token: 'verySecret',
- project: @old_project
- )
- end
-
- it "performs http reuquest to ci" do
- stub_request(:post, "http://ci.gitlab.org/api/v1/forks")
- @service.fork_registration(@project, @user.private_token)
- end
- end
end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 65d16beef91..f67d7b30980 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -87,7 +87,7 @@ describe HipchatService do
it "should create a push message" do
message = hipchat.send(:create_push_message, push_sample_data)
- obj_attr = push_sample_data[:object_attributes]
+ push_sample_data[:object_attributes]
branch = push_sample_data[:ref].gsub('refs/heads/', '')
expect(message).to include("#{user.name} pushed to branch " \
"<a href=\"#{project.web_url}/commits/#{branch}\">#{branch}</a> of " \
@@ -107,7 +107,7 @@ describe HipchatService do
it "should create a tag push message" do
message = hipchat.send(:create_push_message, push_sample_data)
- obj_attr = push_sample_data[:object_attributes]
+ push_sample_data[:object_attributes]
expect(message).to eq("#{user.name} pushed new tag " \
"<a href=\"#{project.web_url}/commits/test\">test</a> to " \
"<a href=\"#{project.web_url}\">#{project_name}</a>\n")
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2fcbd5ae108..8b5d2c3a1c1 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -140,7 +140,7 @@ describe Project do
describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do
- last_activity_event = create(:event, project: project)
+ create(:event, project: project)
expect(project.last_activity_at.to_i).to eq(last_event.created_at.to_i)
end
@@ -220,6 +220,7 @@ describe Project do
end
it { expect(Project.find_with_namespace('gitlab/gitlabhq')).to eq(@project) }
+ it { expect(Project.find_with_namespace('GitLab/GitlabHQ')).to eq(@project) }
it { expect(Project.find_with_namespace('gitlab-ci')).to be_nil }
end
end
@@ -401,4 +402,25 @@ describe Project do
it { should eq "http://localhost#{avatar_path}" }
end
end
+
+ describe :ci_commit do
+ let(:project) { create :project }
+ let(:commit) { create :ci_commit, gl_project: project }
+
+ before do
+ project.ensure_gitlab_ci_project
+ project.create_gitlab_ci_service(active: true)
+ end
+
+ it { expect(project.ci_commit(commit.sha)).to eq(commit) }
+ end
+
+ describe :enable_ci do
+ let(:project) { create :project }
+
+ before { project.enable_ci }
+
+ it { expect(project.gitlab_ci?).to be_truthy }
+ it { expect(project.gitlab_ci_project).to be_a(Ci::Project) }
+ end
end
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index cc1138490a0..26e8fdae472 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -66,4 +66,16 @@ describe ProjectTeam do
it { expect(project.team.member?(guest)).to be_truthy }
end
end
+
+ describe "#human_max_access" do
+ it "return master role" do
+ user = create :user
+ group = create :group
+ group.add_users([user.id], GroupMember::MASTER)
+ project = create(:project, namespace: group)
+ project.team << [user, :guest]
+
+ expect(project.team.human_max_access(user.id)).to eq("Master")
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index f785203af7d..94802dcfb79 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -231,7 +231,7 @@ describe ProjectWiki do
end
def commit_details
- commit = { name: user.name, email: user.email, message: "test commit" }
+ { name: user.name, email: user.email, message: "test commit" }
end
def create_page(name, content)
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a46e789eab4..c71cfb3ebe3 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -2,62 +2,58 @@
#
# Table name: users
#
-# id :integer not null, primary key
-# email :string(255) default(""), not null
-# encrypted_password :string(255) default(""), not null
-# reset_password_token :string(255)
-# reset_password_sent_at :datetime
-# remember_created_at :datetime
-# sign_in_count :integer default(0)
-# current_sign_in_at :datetime
-# last_sign_in_at :datetime
-# current_sign_in_ip :string(255)
-# last_sign_in_ip :string(255)
-# created_at :datetime
-# updated_at :datetime
-# name :string(255)
-# admin :boolean default(FALSE), not null
-# projects_limit :integer default(10)
-# skype :string(255) default(""), not null
-# linkedin :string(255) default(""), not null
-# twitter :string(255) default(""), not null
-# authentication_token :string(255)
-# theme_id :integer default(1), not null
-# bio :string(255)
-# failed_attempts :integer default(0)
-# locked_at :datetime
-# username :string(255)
-# can_create_group :boolean default(TRUE), not null
-# can_create_team :boolean default(TRUE), not null
-# state :string(255)
-# color_scheme_id :integer default(1), not null
-# notification_level :integer default(1), not null
-# password_expires_at :datetime
-# created_by_id :integer
-# last_credential_check_at :datetime
-# avatar :string(255)
-# confirmation_token :string(255)
-# confirmed_at :datetime
-# confirmation_sent_at :datetime
-# unconfirmed_email :string(255)
-# hide_no_ssh_key :boolean default(FALSE)
-# website_url :string(255) default(""), not null
-# github_access_token :string(255)
-# gitlab_access_token :string(255)
-# notification_email :string(255)
-# hide_no_password :boolean default(FALSE)
-# password_automatically_set :boolean default(FALSE)
-# bitbucket_access_token :string(255)
-# bitbucket_access_token_secret :string(255)
-# location :string(255)
-# encrypted_otp_secret :string(255)
-# encrypted_otp_secret_iv :string(255)
-# encrypted_otp_secret_salt :string(255)
-# otp_required_for_login :boolean default(FALSE), not null
-# otp_backup_codes :text
-# public_email :string(255) default(""), not null
-# dashboard :integer default(0)
-# project_view :integer default(0)
+# id :integer not null, primary key
+# email :string(255) default(""), not null
+# encrypted_password :string(255) default(""), not null
+# reset_password_token :string(255)
+# reset_password_sent_at :datetime
+# remember_created_at :datetime
+# sign_in_count :integer default(0)
+# current_sign_in_at :datetime
+# last_sign_in_at :datetime
+# current_sign_in_ip :string(255)
+# last_sign_in_ip :string(255)
+# created_at :datetime
+# updated_at :datetime
+# name :string(255)
+# admin :boolean default(FALSE), not null
+# projects_limit :integer default(10)
+# skype :string(255) default(""), not null
+# linkedin :string(255) default(""), not null
+# twitter :string(255) default(""), not null
+# authentication_token :string(255)
+# theme_id :integer default(1), not null
+# bio :string(255)
+# failed_attempts :integer default(0)
+# locked_at :datetime
+# username :string(255)
+# can_create_group :boolean default(TRUE), not null
+# can_create_team :boolean default(TRUE), not null
+# state :string(255)
+# color_scheme_id :integer default(1), not null
+# notification_level :integer default(1), not null
+# password_expires_at :datetime
+# created_by_id :integer
+# last_credential_check_at :datetime
+# avatar :string(255)
+# confirmation_token :string(255)
+# confirmed_at :datetime
+# confirmation_sent_at :datetime
+# unconfirmed_email :string(255)
+# hide_no_ssh_key :boolean default(FALSE)
+# website_url :string(255) default(""), not null
+# notification_email :string(255)
+# hide_no_password :boolean default(FALSE)
+# password_automatically_set :boolean default(FALSE)
+# location :string(255)
+# encrypted_otp_secret :string(255)
+# encrypted_otp_secret_iv :string(255)
+# encrypted_otp_secret_salt :string(255)
+# otp_required_for_login :boolean default(FALSE), not null
+# otp_backup_codes :text
+# public_email :string(255) default(""), not null
+# dashboard :integer default(0)
+# project_view :integer default(0)
#
require 'spec_helper'
@@ -89,6 +85,7 @@ describe User do
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:assigned_merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
+ it { is_expected.to have_one(:abuse_report) }
end
describe 'validations' do
@@ -192,7 +189,7 @@ describe User do
end
it 'confirms a user' do
- user.confirm!
+ user.confirm
expect(user.confirmed?).to be_truthy
end
end
@@ -231,6 +228,26 @@ describe User do
end
end
+ describe '#recently_sent_password_reset?' do
+ it 'is false when reset_password_sent_at is nil' do
+ user = build_stubbed(:user, reset_password_sent_at: nil)
+
+ expect(user.recently_sent_password_reset?).to eq false
+ end
+
+ it 'is false when sent more than one minute ago' do
+ user = build_stubbed(:user, reset_password_sent_at: 5.minutes.ago)
+
+ expect(user.recently_sent_password_reset?).to eq false
+ end
+
+ it 'is true when sent less than one minute ago' do
+ user = build_stubbed(:user, reset_password_sent_at: Time.now)
+
+ expect(user.recently_sent_password_reset?).to eq true
+ end
+ end
+
describe '#disable_two_factor!' do
it 'clears all 2FA-related fields' do
user = create(:user, :two_factor)
diff --git a/spec/models/wiki_page_spec.rb b/spec/models/wiki_page_spec.rb
index dc84a14bb40..d7802d1734f 100644
--- a/spec/models/wiki_page_spec.rb
+++ b/spec/models/wiki_page_spec.rb
@@ -196,7 +196,7 @@ describe WikiPage do
end
def commit_details
- commit = { name: user.name, email: user.email, message: "test commit" }
+ { name: user.name, email: user.email, message: "test commit" }
end
def create_page(name, content)
diff --git a/spec/requests/api/keys_spec.rb b/spec/requests/api/keys_spec.rb
new file mode 100644
index 00000000000..d2b87f88712
--- /dev/null
+++ b/spec/requests/api/keys_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
+ let(:key) { create(:key, user: user) }
+ let(:email) { create(:email, user: user) }
+
+ describe 'GET /keys/:uid' do
+ before { admin }
+
+ context 'when unauthenticated' do
+ it 'should return authentication error' do
+ get api("/keys/#{key.id}")
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'when authenticated' do
+ it 'should return 404 for non-existing key' do
+ get api('/keys/999999', admin)
+ expect(response.status).to eq(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+
+ it 'should return single ssh key with user information' do
+ user.keys << key
+ user.save
+ get api("/keys/#{key.id}", admin)
+ expect(response.status).to eq(200)
+ expect(json_response['title']).to eq(key.title)
+ expect(json_response['user']['id']).to eq(user.id)
+ expect(json_response['user']['username']).to eq(user.username)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 942768fa254..35b3d3e296a 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -2,11 +2,12 @@ require "spec_helper"
describe API::API, api: true do
include ApiHelpers
+ let(:base_time) { Time.now }
let(:user) { create(:user) }
let!(:project) {create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test") }
- let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test") }
- let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test") }
+ let!(:merge_request) { create(:merge_request, :simple, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
+ let!(:merge_request_closed) { create(:merge_request, state: "closed", author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.seconds) }
+ let!(:merge_request_merged) { create(:merge_request, state: "merged", author: user, assignee: user, source_project: project, target_project: project, title: "Merged test", created_at: base_time + 2.seconds) }
let!(:note) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "a comment on a MR") }
let!(:note2) { create(:note_on_merge_request, author: user, project: project, noteable: merge_request, note: "another comment on a MR") }
@@ -74,8 +75,8 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- expect(json_response.last['id']).to eq(@mr_earlier.id)
- expect(json_response.first['id']).to eq(@mr_later.id)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
end
it "should return an array of merge_requests in descending order" do
@@ -83,8 +84,8 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- expect(json_response.first['id']).to eq(@mr_later.id)
- expect(json_response.last['id']).to eq(@mr_earlier.id)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
end
it "should return an array of merge_requests ordered by updated_at" do
@@ -92,17 +93,17 @@ describe API::API, api: true do
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- expect(json_response.last['id']).to eq(@mr_earlier.id)
- expect(json_response.first['id']).to eq(@mr_later.id)
+ response_dates = json_response.map{ |merge_request| merge_request['updated_at'] }
+ expect(response_dates).to eq(response_dates.sort.reverse)
end
it "should return an array of merge_requests ordered by created_at" do
- get api("/projects/#{project.id}/merge_requests?sort=created_at", user)
+ get api("/projects/#{project.id}/merge_requests?order_by=created_at&sort=asc", user)
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
- expect(json_response.last['id']).to eq(@mr_earlier.id)
- expect(json_response.first['id']).to eq(@mr_later.id)
+ response_dates = json_response.map{ |merge_request| merge_request['created_at'] }
+ expect(response_dates).to eq(response_dates.sort)
end
end
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 5037575d355..606b226ad77 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -5,7 +5,7 @@ describe API::API, 'ProjectHooks', api: true do
let(:user) { create(:user) }
let(:user3) { create(:user) }
let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
- let!(:hook) { create(:project_hook, project: project, url: "http://example.com") }
+ let!(:hook) { create(:project_hook, project: project, url: "http://example.com", push_events: true, merge_requests_events: true, tag_push_events: true, issues_events: true, note_events: true, enable_ssl_verification: true) }
before do
project.team << [user, :master]
@@ -21,6 +21,12 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response).to be_an Array
expect(json_response.count).to eq(1)
expect(json_response.first['url']).to eq("http://example.com")
+ expect(json_response.first['issues_events']).to eq(true)
+ expect(json_response.first['push_events']).to eq(true)
+ expect(json_response.first['merge_requests_events']).to eq(true)
+ expect(json_response.first['tag_push_events']).to eq(true)
+ expect(json_response.first['note_events']).to eq(true)
+ expect(json_response.first['enable_ssl_verification']).to eq(true)
end
end
@@ -38,6 +44,12 @@ describe API::API, 'ProjectHooks', api: true do
get api("/projects/#{project.id}/hooks/#{hook.id}", user)
expect(response.status).to eq(200)
expect(json_response['url']).to eq(hook.url)
+ expect(json_response['issues_events']).to eq(hook.issues_events)
+ expect(json_response['push_events']).to eq(hook.push_events)
+ expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
+ expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
+ expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
it "should return a 404 error if hook id is not available" do
@@ -65,6 +77,13 @@ describe API::API, 'ProjectHooks', api: true do
post api("/projects/#{project.id}/hooks", user), url: "http://example.com", issues_events: true
end.to change {project.hooks.count}.by(1)
expect(response.status).to eq(201)
+ expect(json_response['url']).to eq('http://example.com')
+ expect(json_response['issues_events']).to eq(true)
+ expect(json_response['push_events']).to eq(true)
+ expect(json_response['merge_requests_events']).to eq(false)
+ expect(json_response['tag_push_events']).to eq(false)
+ expect(json_response['note_events']).to eq(false)
+ expect(json_response['enable_ssl_verification']).to eq(true)
end
it "should return a 400 error if url not given" do
@@ -84,6 +103,12 @@ describe API::API, 'ProjectHooks', api: true do
url: 'http://example.org', push_events: false
expect(response.status).to eq(200)
expect(json_response['url']).to eq('http://example.org')
+ expect(json_response['issues_events']).to eq(hook.issues_events)
+ expect(json_response['push_events']).to eq(false)
+ expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
+ expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
+ expect(json_response['note_events']).to eq(hook.note_events)
+ expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
it "should return 404 error if hook id not found" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 5bd8206b890..580bbec77d1 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -89,7 +89,7 @@ describe API::API, api: true do
it 'returns projects in the correct order when ci_enabled_first parameter is passed' do
[project, project2, project3].each{ |project| project.build_missing_services }
- project2.gitlab_ci_service.update(active: true, token: "token", project_url: "http://ci.example.com/projects/1")
+ project2.gitlab_ci_service.update(active: true)
get api('/projects', user), { ci_enabled_first: 'true' }
expect(response.status).to eq(200)
expect(json_response).to be_an Array
diff --git a/spec/requests/api/services_spec.rb b/spec/requests/api/services_spec.rb
index 6d29a28580a..9aa60826f21 100644
--- a/spec/requests/api/services_spec.rb
+++ b/spec/requests/api/services_spec.rb
@@ -5,64 +5,57 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:project) {create(:project, creator_id: user.id, namespace: user.namespace) }
- describe "POST /projects/:id/services/gitlab-ci" do
- it "should update gitlab-ci settings" do
- put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'secrettoken', project_url: "http://ci.example.com/projects/1"
+ Service.available_services_names.each do |service|
+ describe "PUT /projects/:id/services/#{service.dasherize}" do
+ include_context service
- expect(response.status).to eq(200)
- end
-
- it "should return if required fields missing" do
- put api("/projects/#{project.id}/services/gitlab-ci", user), project_url: "http://ci.example.com/projects/1", active: true
+ it "should update #{service} settings" do
+ put api("/projects/#{project.id}/services/#{dashed_service}", user), service_attrs
- expect(response.status).to eq(400)
- end
+ expect(response.status).to eq(200)
+ end
- it "should return if the format of token is invalid" do
- put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'token-with dashes and spaces%', project_url: "http://ci.example.com/projects/1", active: true
+ it "should return if required fields missing" do
+ attrs = service_attrs
- expect(response.status).to eq(404)
- end
+ required_attributes = service_attrs_list.select do |attr|
+ service_klass.validators_on(attr).any? do |v|
+ v.class == ActiveRecord::Validations::PresenceValidator
+ end
+ end
- it "should return if the format of token is invalid" do
- put api("/projects/#{project.id}/services/gitlab-ci", user), token: 'token-with dashes and spaces%', project_url: "ftp://ci.example/projects/1", active: true
+ if required_attributes.empty?
+ expected_code = 200
+ else
+ attrs.delete(required_attributes.shuffle.first)
+ expected_code = 400
+ end
+
+ put api("/projects/#{project.id}/services/#{dashed_service}", user), attrs
- expect(response.status).to eq(404)
+ expect(response.status).to eq(expected_code)
+ end
end
- end
- describe "DELETE /projects/:id/services/gitlab-ci" do
- it "should update gitlab-ci settings" do
- delete api("/projects/#{project.id}/services/gitlab-ci", user)
+ describe "DELETE /projects/:id/services/#{service.dasherize}" do
+ include_context service
- expect(response.status).to eq(200)
- expect(project.gitlab_ci_service).to be_nil
- end
- end
-
- describe 'PUT /projects/:id/services/hipchat' do
- it 'should update hipchat settings' do
- put api("/projects/#{project.id}/services/hipchat", user),
- token: 'secret-token', room: 'test'
+ it "should delete #{service}" do
+ delete api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response.status).to eq(200)
- expect(project.hipchat_service).not_to be_nil
+ expect(response.status).to eq(200)
+ expect(project.send(service_method).activated?).to be_falsey
+ end
end
- it 'should return if required fields missing' do
- put api("/projects/#{project.id}/services/gitlab-ci", user),
- token: 'secret-token', active: true
-
- expect(response.status).to eq(400)
- end
- end
+ describe "GET /projects/:id/services/#{service.dasherize}" do
+ include_context service
- describe 'DELETE /projects/:id/services/hipchat' do
- it 'should delete hipchat settings' do
- delete api("/projects/#{project.id}/services/hipchat", user)
+ it "should get #{service} settings" do
+ get api("/projects/#{project.id}/services/#{dashed_service}", user)
- expect(response.status).to eq(200)
- expect(project.hipchat_service).to be_nil
+ expect(response.status).to eq(200)
+ end
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index f2aa369985e..d26a300ed82 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -7,6 +7,7 @@ describe API::API, api: true do
let(:admin) { create(:admin) }
let(:key) { create(:key, user: user) }
let(:email) { create(:email, user: user) }
+ let(:omniauth_user) { create(:omniauth_user) }
describe "GET /users" do
context "when unauthenticated" do
@@ -58,6 +59,11 @@ describe API::API, api: true do
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 Not found')
end
+
+ it "should return a 404 if invalid ID" do
+ get api("/users/1ASDF", user)
+ expect(response.status).to eq(404)
+ end
end
describe "POST /users" do
@@ -225,6 +231,19 @@ describe API::API, api: true do
expect(user.reload.username).to eq(user.username)
end
+ it "should update user's existing identity" do
+ put api("/users/#{omniauth_user.id}", admin), provider: 'ldapmain', extern_uid: '654321'
+ expect(response.status).to eq(200)
+ expect(omniauth_user.reload.identities.first.extern_uid).to eq('654321')
+ end
+
+ it 'should update user with new identity' do
+ put api("/users/#{user.id}", admin), provider: 'github', extern_uid: '67890'
+ expect(response.status).to eq(200)
+ expect(user.reload.identities.first.extern_uid).to eq('67890')
+ expect(user.reload.identities.first.provider).to eq('github')
+ end
+
it "should update admin status" do
put api("/users/#{user.id}", admin), { admin: true }
expect(response.status).to eq(200)
@@ -257,6 +276,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
+ it "should raise error for invalid ID" do
+ expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ end
+
it 'should return 400 error if user does not validate' do
put api("/users/#{user.id}", admin),
password: 'pass',
@@ -319,6 +342,10 @@ describe API::API, api: true do
post api("/users/#{user.id}/keys", admin), key_attrs
end.to change{ user.keys.count }.by(1)
end
+
+ it "should raise error for invalid ID" do
+ expect{post api("/users/ASDF/keys", admin) }.to raise_error(ActionController::RoutingError)
+ end
end
describe 'GET /user/:uid/keys' do
@@ -346,6 +373,11 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(key.title)
end
+
+ it "should return 404 for invalid ID" do
+ get api("/users/ASDF/keys", admin)
+ expect(response.status).to eq(404)
+ end
end
end
@@ -400,6 +432,10 @@ describe API::API, api: true do
post api("/users/#{user.id}/emails", admin), email_attrs
end.to change{ user.emails.count }.by(1)
end
+
+ it "should raise error for invalid ID" do
+ expect{post api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError)
+ end
end
describe 'GET /user/:uid/emails' do
@@ -427,6 +463,10 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.first['email']).to eq(email.email)
end
+
+ it "should raise error for invalid ID" do
+ expect{put api("/users/ASDF/emails", admin) }.to raise_error(ActionController::RoutingError)
+ end
end
end
@@ -463,6 +503,10 @@ describe API::API, api: true do
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 Email Not Found')
end
+
+ it "should raise error for invalid ID" do
+ expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
+ end
end
end
@@ -491,6 +535,10 @@ describe API::API, api: true do
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 User Not Found')
end
+
+ it "should raise error for invalid ID" do
+ expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ end
end
describe "GET /user" do
@@ -553,6 +601,11 @@ describe API::API, api: true do
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 Not found')
end
+
+ it "should return 404 for invalid ID" do
+ get api("/users/keys/ASDF", admin)
+ expect(response.status).to eq(404)
+ end
end
describe "POST /user/keys" do
@@ -608,6 +661,10 @@ describe API::API, api: true do
delete api("/user/keys/#{key.id}")
expect(response.status).to eq(401)
end
+
+ it "should raise error for invalid ID" do
+ expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ end
end
describe "GET /user/emails" do
@@ -653,6 +710,11 @@ describe API::API, api: true do
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 Not found')
end
+
+ it "should return 404 for invalid ID" do
+ get api("/users/emails/ASDF", admin)
+ expect(response.status).to eq(404)
+ end
end
describe "POST /user/emails" do
@@ -697,6 +759,10 @@ describe API::API, api: true do
delete api("/user/emails/#{email.id}")
expect(response.status).to eq(401)
end
+
+ it "should raise error for invalid ID" do
+ expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ end
end
describe 'PUT /user/:id/block' do
@@ -748,5 +814,9 @@ describe API::API, api: true do
expect(response.status).to eq(404)
expect(json_response['message']).to eq('404 User Not Found')
end
+
+ it "should raise error for invalid ID" do
+ expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
+ end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
new file mode 100644
index 00000000000..54c1d0199f6
--- /dev/null
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -0,0 +1,121 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+
+ let(:runner) { FactoryGirl.create(:ci_runner, tag_list: ["mysql", "ruby"]) }
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
+
+ describe "Builds API for runners" do
+ let(:shared_runner) { FactoryGirl.create(:ci_runner, token: "SharedRunner") }
+ let(:shared_project) { FactoryGirl.create(:ci_project, name: "SharedProject") }
+ let(:shared_gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: shared_project) }
+
+ before do
+ FactoryGirl.create :ci_runner_project, project_id: project.id, runner_id: runner.id
+ end
+
+ describe "POST /builds/register" do
+ it "should start a build" do
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit.create_builds('master', false, nil)
+ build = commit.builds.first
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response['sha']).to eq(build.sha)
+ expect(runner.reload.platform).to eq("darwin")
+ end
+
+ it "should return 404 error if no pending build found" do
+ post ci_api("/builds/register"), token: runner.token
+
+ expect(response.status).to eq(404)
+ end
+
+ it "should return 404 error if no builds for specific runner" do
+ commit = FactoryGirl.create(:ci_commit, gl_project: shared_gl_project)
+ FactoryGirl.create(:ci_build, commit: commit, status: 'pending' )
+
+ post ci_api("/builds/register"), token: runner.token
+
+ expect(response.status).to eq(404)
+ end
+
+ it "should return 404 error if no builds for shared runner" do
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ FactoryGirl.create(:ci_build, commit: commit, status: 'pending' )
+
+ post ci_api("/builds/register"), token: shared_runner.token
+
+ expect(response.status).to eq(404)
+ end
+
+ it "returns options" do
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit.create_builds('master', false, nil)
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+ end
+
+ it "returns variables" do
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+ commit.create_builds('master', false, nil)
+ project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response["variables"]).to eq([
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true },
+ { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+ ])
+ end
+
+ it "returns variables for triggers" do
+ trigger = FactoryGirl.create(:ci_trigger, project: project)
+ commit = FactoryGirl.create(:ci_commit, gl_project: gl_project)
+
+ trigger_request = FactoryGirl.create(:ci_trigger_request_with_variables, commit: commit, trigger: trigger)
+ commit.create_builds('master', false, nil, trigger_request)
+ project.variables << Ci::Variable.new(key: "SECRET_KEY", value: "secret_value")
+
+ post ci_api("/builds/register"), token: runner.token, info: { platform: :darwin }
+
+ expect(response.status).to eq(201)
+ expect(json_response["variables"]).to eq([
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true },
+ { "key" => "SECRET_KEY", "value" => "secret_value", "public" => false },
+ { "key" => "TRIGGER_KEY", "value" => "TRIGGER_VALUE", "public" => false },
+ ])
+ end
+ end
+
+ describe "PUT /builds/:id" do
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project)}
+ let(:build) { FactoryGirl.create(:ci_build, commit: commit, runner_id: runner.id) }
+
+ it "should update a running build" do
+ build.run!
+ put ci_api("/builds/#{build.id}"), token: runner.token
+ expect(response.status).to eq(200)
+ end
+
+ it 'Should not override trace information when no trace is given' do
+ build.run!
+ build.update!(trace: 'hello_world')
+ put ci_api("/builds/#{build.id}"), token: runner.token
+ expect(build.reload.trace).to eq 'hello_world'
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/commits_spec.rb b/spec/requests/ci/api/commits_spec.rb
new file mode 100644
index 00000000000..6049135fd10
--- /dev/null
+++ b/spec/requests/ci/api/commits_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Ci::API::API, 'Commits' do
+ include ApiHelpers
+
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project) }
+
+ let(:options) do
+ {
+ project_token: project.token,
+ project_id: project.id
+ }
+ end
+
+ describe "GET /commits" do
+ before { commit }
+
+ it "should return commits per project" do
+ get ci_api("/commits"), options
+
+ expect(response.status).to eq(200)
+ expect(json_response.count).to eq(1)
+ expect(json_response.first["project_id"]).to eq(project.id)
+ expect(json_response.first["sha"]).to eq(commit.sha)
+ end
+ end
+
+ describe "POST /commits" do
+ let(:data) do
+ {
+ "before" => "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ "after" => "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "ref" => "refs/heads/master",
+ "commits" => [
+ {
+ "id" => "b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "message" => "Update Catalan translation to e38cb41.",
+ "timestamp" => "2011-12-12T14:27:31+02:00",
+ "url" => "http://localhost/diaspora/commits/b6568db1bc1dcd7f8b4d5a946b0b91f9dacd7327",
+ "author" => {
+ "name" => "Jordi Mallach",
+ "email" => "jordi@softcatala.org",
+ }
+ }
+ ]
+ }
+ end
+
+ it "should create a build" do
+ post ci_api("/commits"), options.merge(data: data)
+
+ expect(response.status).to eq(201)
+ expect(json_response['sha']).to eq("da1560886d4f094c3e6c9ef40349f7d38b5d27d7")
+ end
+
+ it "should return 400 error if no data passed" do
+ post ci_api("/commits"), options
+
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq("400 (Bad request) \"data\" not given")
+ end
+ end
+end
diff --git a/spec/requests/ci/api/projects_spec.rb b/spec/requests/ci/api/projects_spec.rb
new file mode 100644
index 00000000000..53f7f91cc1f
--- /dev/null
+++ b/spec/requests/ci/api/projects_spec.rb
@@ -0,0 +1,266 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+
+ let(:gitlab_url) { GitlabCi.config.gitlab_ci.url }
+ let(:user) { create(:user) }
+ let(:private_token) { user.private_token }
+
+ let(:options) do
+ {
+ private_token: private_token,
+ url: gitlab_url
+ }
+ end
+
+ before do
+ stub_gitlab_calls
+ end
+
+ context "requests for scoped projects" do
+ # NOTE: These ids are tied to the actual projects on demo.gitlab.com
+ describe "GET /projects" do
+ let!(:project1) { FactoryGirl.create(:ci_project) }
+ let!(:project2) { FactoryGirl.create(:ci_project) }
+
+ before do
+ project1.gl_project.team << [user, :developer]
+ project2.gl_project.team << [user, :developer]
+ end
+
+ it "should return all projects on the CI instance" do
+ get ci_api("/projects"), options
+ expect(response.status).to eq(200)
+ expect(json_response.count).to eq(2)
+ expect(json_response.first["id"]).to eq(project1.id)
+ expect(json_response.last["id"]).to eq(project2.id)
+ end
+ end
+
+ describe "GET /projects/owned" do
+ let!(:gl_project1) {FactoryGirl.create(:empty_project, namespace: user.namespace)}
+ let!(:gl_project2) {FactoryGirl.create(:empty_project, namespace: user.namespace)}
+ let!(:project1) { FactoryGirl.create(:ci_project, gl_project: gl_project1) }
+ let!(:project2) { FactoryGirl.create(:ci_project, gl_project: gl_project2) }
+
+ before do
+ project1.gl_project.team << [user, :developer]
+ project2.gl_project.team << [user, :developer]
+ end
+
+ it "should return all projects on the CI instance" do
+ get ci_api("/projects/owned"), options
+
+ expect(response.status).to eq(200)
+ expect(json_response.count).to eq(2)
+ end
+ end
+ end
+
+ describe "POST /projects/:project_id/webhooks" do
+ let!(:project) { FactoryGirl.create(:ci_project) }
+
+ context "Valid Webhook URL" do
+ let!(:webhook) { { web_hook: "http://example.com/sth/1/ala_ma_kota" } }
+
+ before do
+ options.merge!(webhook)
+ end
+
+ it "should create webhook for specified project" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/webhooks"), options
+ expect(response.status).to eq(201)
+ expect(json_response["url"]).to eq(webhook[:web_hook])
+ end
+
+ it "fails to create webhook for non existsing project" do
+ post ci_api("/projects/non-existant-id/webhooks"), options
+ expect(response.status).to eq(404)
+ end
+
+ it "non-manager is not authorized" do
+ post ci_api("/projects/#{project.id}/webhooks"), options
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context "Invalid Webhook URL" do
+ let!(:webhook) { { web_hook: "ala_ma_kota" } }
+
+ before do
+ options.merge!(webhook)
+ end
+
+ it "fails to create webhook for not valid url" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/webhooks"), options
+ expect(response.status).to eq(400)
+ end
+ end
+
+ context "Missed web_hook parameter" do
+ it "fails to create webhook for not provided url" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/webhooks"), options
+ expect(response.status).to eq(400)
+ end
+ end
+ end
+
+ describe "GET /projects/:id" do
+ let!(:project) { FactoryGirl.create(:ci_project) }
+
+ before do
+ project.gl_project.team << [user, :developer]
+ end
+
+ context "with an existing project" do
+ it "should retrieve the project info" do
+ get ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(200)
+ expect(json_response['id']).to eq(project.id)
+ end
+ end
+
+ context "with a non-existing project" do
+ it "should return 404 error if project not found" do
+ get ci_api("/projects/non_existent_id"), options
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ describe "PUT /projects/:id" do
+ let!(:project) { FactoryGirl.create(:ci_project) }
+ let!(:project_info) { { default_ref: "develop" } }
+
+ before do
+ options.merge!(project_info)
+ end
+
+ it "should update a specific project's information" do
+ project.gl_project.team << [user, :master]
+ put ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(200)
+ expect(json_response["default_ref"]).to eq(project_info[:default_ref])
+ end
+
+ it "fails to update a non-existing project" do
+ put ci_api("/projects/non-existant-id"), options
+ expect(response.status).to eq(404)
+ end
+
+ it "non-manager is not authorized" do
+ put ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(401)
+ end
+ end
+
+ describe "DELETE /projects/:id" do
+ let!(:project) { FactoryGirl.create(:ci_project) }
+
+ it "should delete a specific project" do
+ project.gl_project.team << [user, :master]
+ delete ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(200)
+ expect { project.reload }.
+ to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it "non-manager is not authorized" do
+ delete ci_api("/projects/#{project.id}"), options
+ expect(response.status).to eq(401)
+ end
+
+ it "is getting not found error" do
+ delete ci_api("/projects/not-existing_id"), options
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe "POST /projects" do
+ let(:gl_project) { FactoryGirl.create :empty_project }
+ let(:project_info) do
+ {
+ gitlab_id: gl_project.id
+ }
+ end
+
+ let(:invalid_project_info) { {} }
+
+ context "with valid project info" do
+ before do
+ options.merge!(project_info)
+ end
+
+ it "should create a project with valid data" do
+ post ci_api("/projects"), options
+ expect(response.status).to eq(201)
+ expect(json_response['name']).to eq(gl_project.name_with_namespace)
+ end
+ end
+
+ context "with invalid project info" do
+ before do
+ options.merge!(invalid_project_info)
+ end
+
+ it "should error with invalid data" do
+ post ci_api("/projects"), options
+ expect(response.status).to eq(400)
+ end
+ end
+
+ describe "POST /projects/:id/runners/:id" do
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:runner) { FactoryGirl.create(:ci_runner) }
+
+ it "should add the project to the runner" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(201)
+
+ project.reload
+ expect(project.runners.first.id).to eq(runner.id)
+ end
+
+ it "should fail if it tries to link a non-existing project or runner" do
+ post ci_api("/projects/#{project.id}/runners/non-existing"), options
+ expect(response.status).to eq(404)
+
+ post ci_api("/projects/non-existing/runners/#{runner.id}"), options
+ expect(response.status).to eq(404)
+ end
+
+ it "non-manager is not authorized" do
+ allow_any_instance_of(User).to receive(:can_manage_project?).and_return(false)
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(401)
+ end
+ end
+
+ describe "DELETE /projects/:id/runners/:id" do
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:runner) { FactoryGirl.create(:ci_runner) }
+
+ it "should remove the project from the runner" do
+ project.gl_project.team << [user, :master]
+ post ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+
+ expect(project.runners).to be_present
+ delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(200)
+
+ project.reload
+ expect(project.runners).to be_empty
+ end
+
+ it "non-manager is not authorized" do
+ delete ci_api("/projects/#{project.id}/runners/#{runner.id}"), options
+ expect(response.status).to eq(401)
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/runners_spec.rb b/spec/requests/ci/api/runners_spec.rb
new file mode 100644
index 00000000000..11dc089e1f5
--- /dev/null
+++ b/spec/requests/ci/api/runners_spec.rb
@@ -0,0 +1,83 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+ include StubGitlabCalls
+
+ before do
+ stub_gitlab_calls
+ end
+
+ describe "GET /runners" do
+ let(:gitlab_url) { GitlabCi.config.gitlab_ci.url }
+ let(:private_token) { create(:user).private_token }
+ let(:options) do
+ {
+ private_token: private_token,
+ url: gitlab_url
+ }
+ end
+
+ before do
+ 5.times { FactoryGirl.create(:ci_runner) }
+ end
+
+ it "should retrieve a list of all runners" do
+ get ci_api("/runners", nil), options
+ expect(response.status).to eq(200)
+ expect(json_response.count).to eq(5)
+ expect(json_response.last).to have_key("id")
+ expect(json_response.last).to have_key("token")
+ end
+ end
+
+ describe "POST /runners/register" do
+ describe "should create a runner if token provided" do
+ before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN }
+
+ it { expect(response.status).to eq(201) }
+ end
+
+ describe "should create a runner with description" do
+ before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, description: "server.hostname" }
+
+ it { expect(response.status).to eq(201) }
+ it { expect(Ci::Runner.first.description).to eq("server.hostname") }
+ end
+
+ describe "should create a runner with tags" do
+ before { post ci_api("/runners/register"), token: GitlabCi::REGISTRATION_TOKEN, tag_list: "tag1, tag2" }
+
+ it { expect(response.status).to eq(201) }
+ it { expect(Ci::Runner.first.tag_list.sort).to eq(["tag1", "tag2"]) }
+ end
+
+ describe "should create a runner if project token provided" do
+ let(:project) { FactoryGirl.create(:ci_project) }
+ before { post ci_api("/runners/register"), token: project.token }
+
+ it { expect(response.status).to eq(201) }
+ it { expect(project.runners.size).to eq(1) }
+ end
+
+ it "should return 403 error if token is invalid" do
+ post ci_api("/runners/register"), token: 'invalid'
+
+ expect(response.status).to eq(403)
+ end
+
+ it "should return 400 error if no token" do
+ post ci_api("/runners/register")
+
+ expect(response.status).to eq(400)
+ end
+ end
+
+ describe "DELETE /runners/delete" do
+ let!(:runner) { FactoryGirl.create(:ci_runner) }
+ before { delete ci_api("/runners/delete"), token: runner.token }
+
+ it { expect(response.status).to eq(200) }
+ it { expect(Ci::Runner.count).to eq(0) }
+ end
+end
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
new file mode 100644
index 00000000000..93617fc4b3f
--- /dev/null
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -0,0 +1,81 @@
+require 'spec_helper'
+
+describe Ci::API::API do
+ include ApiHelpers
+
+ describe 'POST /projects/:project_id/refs/:ref/trigger' do
+ let!(:trigger_token) { 'secure token' }
+ let!(:gl_project) { FactoryGirl.create(:project) }
+ let!(:project) { FactoryGirl.create(:ci_project, gl_project: gl_project) }
+ let!(:project2) { FactoryGirl.create(:ci_project) }
+ let!(:trigger) { FactoryGirl.create(:ci_trigger, project: project, token: trigger_token) }
+ let(:options) do
+ {
+ token: trigger_token
+ }
+ end
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
+
+ context 'Handles errors' do
+ it 'should return bad request if token is missing' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger")
+ expect(response.status).to eq(400)
+ end
+
+ it 'should return not found if project is not found' do
+ post ci_api('/projects/0/refs/master/trigger'), options
+ expect(response.status).to eq(404)
+ end
+
+ it 'should return unauthorized if token is for different project' do
+ post ci_api("/projects/#{project2.id}/refs/master/trigger"), options
+ expect(response.status).to eq(401)
+ end
+ end
+
+ context 'Have a commit' do
+ let(:commit) { project.commits.last }
+
+ it 'should create builds' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger"), options
+ expect(response.status).to eq(201)
+ commit.builds.reload
+ expect(commit.builds.size).to eq(2)
+ end
+
+ it 'should return bad request with no builds created if there\'s no commit for that ref' do
+ post ci_api("/projects/#{project.id}/refs/other-branch/trigger"), options
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('No builds created')
+ end
+
+ context 'Validates variables' do
+ let(:variables) do
+ { 'TRIGGER_KEY' => 'TRIGGER_VALUE' }
+ end
+
+ it 'should validate variables to be a hash' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: 'value')
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('variables needs to be a hash')
+ end
+
+ it 'should validate variables needs to be a map of key-valued strings' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: { key: %w(1 2) })
+ expect(response.status).to eq(400)
+ expect(json_response['message']).to eq('variables needs to be a map of key-valued strings')
+ end
+
+ it 'create trigger request with variables' do
+ post ci_api("/projects/#{project.id}/refs/master/trigger"), options.merge(variables: variables)
+ expect(response.status).to eq(201)
+ commit.builds.reload
+ expect(commit.builds.first.trigger_request.variables).to eq(variables)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/builds_spec.rb b/spec/requests/ci/builds_spec.rb
new file mode 100644
index 00000000000..f68116c52aa
--- /dev/null
+++ b/spec/requests/ci/builds_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe "Builds" do
+ before do
+ @commit = FactoryGirl.create :ci_commit
+ @build = FactoryGirl.create :ci_build, commit: @commit
+ end
+
+ describe "GET /:project/builds/:id/status.json" do
+ before do
+ get status_ci_project_build_path(@commit.project, @build), format: :json
+ end
+
+ it { expect(response.status).to eq(200) }
+ it { expect(response.body).to include(@build.sha) }
+ end
+end
diff --git a/spec/requests/ci/commits_spec.rb b/spec/requests/ci/commits_spec.rb
new file mode 100644
index 00000000000..f43a3982d71
--- /dev/null
+++ b/spec/requests/ci/commits_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe "Commits" do
+ before do
+ @commit = FactoryGirl.create :ci_commit
+ end
+
+ describe "GET /:project/refs/:ref_name/commits/:id/status.json" do
+ before do
+ get status_ci_project_commits_path(@commit.project, @commit.sha), format: :json
+ end
+
+ it { expect(response.status).to eq(200) }
+ it { expect(response.body).to include(@commit.sha) }
+ end
+end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index dd045826692..dfa18f69e05 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -206,7 +206,7 @@ end
# dashboard_merge_requests GET /dashboard/merge_requests(.:format) dashboard#merge_requests
describe DashboardController, "routing" do
it "to #index" do
- expect(get("/dashboard")).to route_to('dashboard#show')
+ expect(get("/dashboard")).to route_to('dashboard/projects#index')
end
it "to #issues" do
@@ -220,8 +220,8 @@ end
# root / root#show
describe RootController, 'routing' do
- it 'to #show' do
- expect(get('/')).to route_to('root#show')
+ it 'to #index' do
+ expect(get('/')).to route_to('root#index')
end
end
diff --git a/spec/services/ci/create_commit_service_spec.rb b/spec/services/ci/create_commit_service_spec.rb
new file mode 100644
index 00000000000..e3a8fe9681b
--- /dev/null
+++ b/spec/services/ci/create_commit_service_spec.rb
@@ -0,0 +1,154 @@
+require 'spec_helper'
+
+module Ci
+ describe CreateCommitService do
+ let(:service) { CreateCommitService.new }
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:user) { nil }
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
+
+ describe :execute do
+ context 'valid params' do
+ let(:commit) do
+ service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: [ { message: "Message" } ]
+ )
+ end
+
+ it { expect(commit).to be_kind_of(Commit) }
+ it { expect(commit).to be_valid }
+ it { expect(commit).to be_persisted }
+ it { expect(commit).to eq(project.commits.last) }
+ it { expect(commit.builds.first).to be_kind_of(Build) }
+ end
+
+ context "skip tag if there is no build for it" do
+ it "creates commit if there is appropriate job" do
+ result = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: [ { message: "Message" } ]
+ )
+ expect(result).to be_persisted
+ end
+
+ it "creates commit if there is no appropriate job but deploy job has right ref setting" do
+ config = YAML.dump({ deploy: { deploy: "ls", only: ["0_1"] } })
+ stub_ci_commit_yaml_file(config)
+
+ result = service.execute(project, user,
+ ref: 'refs/heads/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: [ { message: "Message" } ]
+ )
+ expect(result).to be_persisted
+ end
+ end
+
+ it 'fails commits without .gitlab-ci.yml' do
+ stub_ci_commit_yaml_file(nil)
+ result = service.execute(project, user,
+ ref: 'refs/heads/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: [ { message: 'Message' } ]
+ )
+ expect(result).to be_persisted
+ expect(result.builds.any?).to be_falsey
+ expect(result.status).to eq('failed')
+ end
+
+ describe :ci_skip? do
+ let(:message) { "some message[ci skip]" }
+
+ before do
+ allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { message }
+ end
+
+ it "skips builds creation if there is [ci skip] tag in commit message" do
+ commits = [{ message: message }]
+ commit = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(commit.builds.any?).to be false
+ expect(commit.status).to eq("skipped")
+ end
+
+ it "does not skips builds creation if there is no [ci skip] tag in commit message" do
+ allow_any_instance_of(Ci::Commit).to receive(:git_commit_message) { "some message" }
+
+ commits = [{ message: "some message" }]
+ commit = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(commit.builds.first.name).to eq("staging")
+ end
+
+ it "skips builds creation if there is [ci skip] tag in commit message and yaml is invalid" do
+ stub_ci_commit_yaml_file('invalid: file')
+ commits = [{ message: message }]
+ commit = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(commit.builds.any?).to be false
+ expect(commit.status).to eq("skipped")
+ end
+ end
+
+ it "skips build creation if there are already builds" do
+ allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { gitlab_ci_yaml }
+
+ commits = [{ message: "message" }]
+ commit = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(commit.builds.count(:all)).to eq(2)
+
+ commit = service.execute(project, user,
+ ref: 'refs/heads/master',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+ expect(commit.builds.count(:all)).to eq(2)
+ end
+
+ it "creates commit with failed status if yaml is invalid" do
+ stub_ci_commit_yaml_file('invalid: file')
+
+ commits = [{ message: "some message" }]
+
+ commit = service.execute(project, user,
+ ref: 'refs/tags/0_1',
+ before: '00000000',
+ after: '31das312',
+ commits: commits
+ )
+
+ expect(commit.status).to eq("failed")
+ expect(commit.builds.any?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
new file mode 100644
index 00000000000..fcafae38644
--- /dev/null
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Ci::CreateTriggerRequestService do
+ let(:service) { Ci::CreateTriggerRequestService.new }
+ let(:gl_project) { create(:project) }
+ let(:project) { create(:ci_project, gl_project: gl_project) }
+ let(:trigger) { create(:ci_trigger, project: project) }
+
+ before do
+ stub_ci_commit_to_return_yaml_file
+ end
+
+ describe :execute do
+ context 'valid params' do
+ subject { service.execute(project, trigger, 'master') }
+
+ it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
+ end
+
+ context 'no commit for ref' do
+ subject { service.execute(project, trigger, 'other-branch') }
+
+ it { expect(subject).to be_nil }
+ end
+
+ context 'no builds created' do
+ subject { service.execute(project, trigger, 'master') }
+
+ before do
+ stub_ci_commit_yaml_file('{}')
+ FactoryGirl.create :ci_commit, gl_project: gl_project
+ end
+
+ it { expect(subject).to be_nil }
+ end
+ end
+end
diff --git a/spec/services/ci/event_service_spec.rb b/spec/services/ci/event_service_spec.rb
new file mode 100644
index 00000000000..1264e17ff5e
--- /dev/null
+++ b/spec/services/ci/event_service_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Ci::EventService do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:user) { double(username: "root", id: 1) }
+
+ before do
+ Event.destroy_all
+ end
+
+ describe :remove_project do
+ it "creates event" do
+ Ci::EventService.new.remove_project(user, project)
+
+ expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been removed by root")
+ end
+ end
+
+ describe :create_project do
+ it "creates event" do
+ Ci::EventService.new.create_project(user, project)
+
+ expect(Ci::Event.admin.last.description).to eq("Project \"#{project.name_with_namespace}\" has been created by root")
+ end
+ end
+
+ describe :change_project_settings do
+ it "creates event" do
+ Ci::EventService.new.change_project_settings(user, project)
+
+ expect(Ci::Event.last.description).to eq("User \"root\" updated projects settings")
+ end
+ end
+end
diff --git a/spec/services/ci/image_for_build_service_spec.rb b/spec/services/ci/image_for_build_service_spec.rb
new file mode 100644
index 00000000000..d7242d684c6
--- /dev/null
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+module Ci
+ describe ImageForBuildService do
+ let(:service) { ImageForBuildService.new }
+ let(:project) { FactoryGirl.create(:ci_project) }
+ let(:gl_project) { FactoryGirl.create(:empty_project, gitlab_ci_project: project) }
+ let(:commit) { FactoryGirl.create(:ci_commit, gl_project: gl_project, ref: 'master') }
+ let(:build) { FactoryGirl.create(:ci_build, commit: commit) }
+
+ describe :execute do
+ before { build }
+
+ context 'branch name' do
+ before { build.run! }
+ let(:image) { service.execute(project, ref: 'master') }
+
+ it { expect(image).to be_kind_of(OpenStruct) }
+ it { expect(image.path.to_s).to include('public/ci/build-running.svg') }
+ it { expect(image.name).to eq('build-running.svg') }
+ end
+
+ context 'unknown branch name' do
+ let(:image) { service.execute(project, ref: 'feature') }
+
+ it { expect(image).to be_kind_of(OpenStruct) }
+ it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') }
+ it { expect(image.name).to eq('build-unknown.svg') }
+ end
+
+ context 'commit sha' do
+ before { build.run! }
+ let(:image) { service.execute(project, sha: build.sha) }
+
+ it { expect(image).to be_kind_of(OpenStruct) }
+ it { expect(image.path.to_s).to include('public/ci/build-running.svg') }
+ it { expect(image.name).to eq('build-running.svg') }
+ end
+
+ context 'unknown commit sha' do
+ let(:image) { service.execute(project, sha: '0000000') }
+
+ it { expect(image).to be_kind_of(OpenStruct) }
+ it { expect(image.path.to_s).to include('public/ci/build-unknown.svg') }
+ it { expect(image.name).to eq('build-unknown.svg') }
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
new file mode 100644
index 00000000000..781764627ac
--- /dev/null
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -0,0 +1,90 @@
+require 'spec_helper'
+
+module Ci
+ describe RegisterBuildService do
+ let!(:service) { RegisterBuildService.new }
+ let!(:gl_project) { FactoryGirl.create :empty_project }
+ let!(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+ let!(:pending_build) { FactoryGirl.create :ci_build, commit: commit }
+ let!(:shared_runner) { FactoryGirl.create(:ci_runner, is_shared: true) }
+ let!(:specific_runner) { FactoryGirl.create(:ci_runner, is_shared: false) }
+
+ before do
+ specific_runner.assign_to(gl_project.ensure_gitlab_ci_project)
+ end
+
+ describe :execute do
+ context 'runner follow tag list' do
+ it "picks build with the same tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ specific_runner.tag_list = ["linux"]
+ expect(service.execute(specific_runner)).to eq(pending_build)
+ end
+
+ it "does not pick build with different tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ specific_runner.tag_list = ["win32"]
+ expect(service.execute(specific_runner)).to be_falsey
+ end
+
+ it "picks build without tag" do
+ expect(service.execute(specific_runner)).to eq(pending_build)
+ end
+
+ it "does not pick build with tag" do
+ pending_build.tag_list = ["linux"]
+ pending_build.save
+ expect(service.execute(specific_runner)).to be_falsey
+ end
+
+ it "pick build without tag" do
+ specific_runner.tag_list = ["win32"]
+ expect(service.execute(specific_runner)).to eq(pending_build)
+ end
+ end
+
+ context 'allow shared runners' do
+ before do
+ gl_project.gitlab_ci_project.update(shared_runners_enabled: true)
+ end
+
+ context 'shared runner' do
+ let(:build) { service.execute(shared_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(shared_runner) }
+ end
+
+ context 'specific runner' do
+ let(:build) { service.execute(specific_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(specific_runner) }
+ end
+ end
+
+ context 'disallow shared runners' do
+ context 'shared runner' do
+ let(:build) { service.execute(shared_runner) }
+
+ it { expect(build).to be_nil }
+ end
+
+ context 'specific runner' do
+ let(:build) { service.execute(specific_runner) }
+
+ it { expect(build).to be_kind_of(Build) }
+ it { expect(build).to be_valid }
+ it { expect(build).to be_running }
+ it { expect(build.runner).to eq(specific_runner) }
+ end
+ end
+ end
+ end
+end
diff --git a/spec/services/ci/web_hook_service_spec.rb b/spec/services/ci/web_hook_service_spec.rb
new file mode 100644
index 00000000000..aa48fcbcbfd
--- /dev/null
+++ b/spec/services/ci/web_hook_service_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Ci::WebHookService do
+ let(:project) { FactoryGirl.create :ci_project }
+ let(:gl_project) { FactoryGirl.create :empty_project, gitlab_ci_project: project }
+ let(:commit) { FactoryGirl.create :ci_commit, gl_project: gl_project }
+ let(:build) { FactoryGirl.create :ci_build, commit: commit }
+ let(:hook) { FactoryGirl.create :ci_web_hook, project: project }
+
+ describe :execute do
+ it "should execute successfully" do
+ stub_request(:post, hook.url).to_return(status: 200)
+ expect(Ci::WebHookService.new.build_end(build)).to be_truthy
+ end
+ end
+
+ context 'build_data' do
+ it "contains all needed fields" do
+ expect(build_data(build)).to include(
+ :build_id,
+ :project_id,
+ :ref,
+ :build_status,
+ :build_started_at,
+ :build_finished_at,
+ :before_sha,
+ :project_name,
+ :gitlab_url,
+ :build_name
+ )
+ end
+ end
+
+ def build_data(build)
+ Ci::WebHookService.new.send :build_data, build
+ end
+end
diff --git a/spec/services/event_create_service_spec.rb b/spec/services/event_create_service_spec.rb
index 007a9eed192..7756b973ecd 100644
--- a/spec/services/event_create_service_spec.rb
+++ b/spec/services/event_create_service_spec.rb
@@ -99,5 +99,15 @@ describe EventCreateService do
expect { service.close_milestone(milestone, user) }.to change { Event.count }
end
end
+
+ describe :destroy_mr do
+ let(:milestone) { create(:milestone) }
+
+ it { expect(service.destroy_milestone(milestone, user)).to be_truthy }
+
+ it "should create new event" do
+ expect { service.destroy_milestone(milestone, user) }.to change { Event.count }
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 7b564d34d7b..7483f51de03 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -35,5 +35,19 @@ describe MergeRequests::MergeService do
expect(note.note).to include 'Status changed to merged'
end
end
+
+ context "error handling" do
+ let(:service) { MergeRequests::MergeService.new(project, user, {}) }
+
+ it 'saves error if there is an exception' do
+ allow(service).to receive(:repository).and_raise("error")
+
+ allow(service).to receive(:execute_hooks)
+
+ service.execute(merge_request, 'Awesome message')
+
+ expect(merge_request.merge_error).to eq("Something went wrong during merge")
+ end
+ end
end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 8865335d0d1..520140917aa 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -427,15 +427,15 @@ describe NotificationService do
should_email(@u_watcher.id)
should_email(@u_participating.id)
should_not_email(@u_disabled.id)
- notification.project_was_moved(project)
+ notification.project_was_moved(project, "gitlab/gitlab")
end
def should_email(user_id)
- expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id)
+ expect(Notify).to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab")
end
def should_not_email(user_id)
- expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id)
+ expect(Notify).not_to receive(:project_was_moved_email).with(project.id, user_id, "gitlab/gitlab")
end
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 66cdfd5d758..25277f07482 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -17,6 +17,14 @@ describe Projects::CreateService do
expect(project.services).not_to be_empty
end
+ it 'creates labels on Project creation if there are templates' do
+ Label.create(title: "bug", template: true)
+ project = create_project(@user, @opts)
+ project.reload
+
+ expect(project.labels).not_to be_empty
+ end
+
context 'user namespace' do
before do
@project = create_project(@user, @opts)
@@ -88,6 +96,17 @@ describe Projects::CreateService do
expect(project.saved?).to be(true)
end
end
+
+ context 'repository creation' do
+ it 'should synchronously create the repository' do
+ expect_any_instance_of(Project).to receive(:create_repository)
+
+ project = create_project(@user, @opts)
+ expect(project).to be_valid
+ expect(project.owner).to eq(@user)
+ expect(project.namespace).to eq(@user.namespace)
+ end
+ end
end
def create_project(user, opts)
diff --git a/spec/services/projects/download_service_spec.rb b/spec/services/projects/download_service_spec.rb
new file mode 100644
index 00000000000..f12e09c58c3
--- /dev/null
+++ b/spec/services/projects/download_service_spec.rb
@@ -0,0 +1,65 @@
+require 'spec_helper'
+
+describe Projects::DownloadService do
+ describe 'File service' do
+ before do
+ @user = create :user
+ @project = create :project, creator_id: @user.id, namespace: @user.namespace
+ end
+
+ context 'for a URL that is not on whitelist' do
+ before do
+ url = 'https://code.jquery.com/jquery-2.1.4.min.js'
+ @link_to_file = download_file(@project, url)
+ end
+
+ it { expect(@link_to_file).to eq(nil) }
+ end
+
+ context 'for URLs that are on the whitelist' do
+ before do
+ sham_rack_app = ShamRack.at('mycompany.fogbugz.com').stub
+ sham_rack_app.register_resource('/rails_sample.jpg', File.read(Rails.root + 'spec/fixtures/rails_sample.jpg'), 'image/jpg')
+ sham_rack_app.register_resource('/doc_sample.txt', File.read(Rails.root + 'spec/fixtures/doc_sample.txt'), 'text/plain')
+ end
+
+ after do
+ ShamRack.unmount_all
+ end
+
+ context 'an image file' do
+ before do
+ url = 'http://mycompany.fogbugz.com/rails_sample.jpg'
+ @link_to_file = download_file(@project, url)
+ end
+
+ it { expect(@link_to_file).to have_key('alt') }
+ it { expect(@link_to_file).to have_key('url') }
+ it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file['is_image']).to be true }
+ it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match('rails_sample.jpg') }
+ it { expect(@link_to_file['alt']).to eq('rails_sample') }
+ end
+
+ context 'a txt file' do
+ before do
+ url = 'http://mycompany.fogbugz.com/doc_sample.txt'
+ @link_to_file = download_file(@project, url)
+ end
+
+ it { expect(@link_to_file).to have_key('alt') }
+ it { expect(@link_to_file).to have_key('url') }
+ it { expect(@link_to_file).to have_key('is_image') }
+ it { expect(@link_to_file['is_image']).to be false }
+ it { expect(@link_to_file['url']).to match("/#{@project.path_with_namespace}") }
+ it { expect(@link_to_file['url']).to match('doc_sample.txt') }
+ it { expect(@link_to_file['alt']).to eq('doc_sample.txt') }
+ end
+ end
+ end
+
+ def download_file(repository, url)
+ Projects::DownloadService.new(repository, url).execute
+ end
+end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index c04e842c67e..65a8c81204d 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -28,8 +28,7 @@ describe Projects::ForkService do
context 'fork project failure' do
it "fails due to transaction failure" do
@to_project = fork_project(@from_project, @to_user, false)
- expect(@to_project.errors).not_to be_empty
- expect(@to_project.errors[:base]).to include("Failed to fork repository via gitlab-shell")
+ expect(@to_project.import_failed?)
end
end
@@ -44,13 +43,10 @@ describe Projects::ForkService do
end
context 'GitLab CI is enabled' do
- it "calls fork registrator for CI" do
- @from_project.build_missing_services
- @from_project.gitlab_ci_service.update_attributes(active: true)
-
- expect(ForkRegistrationWorker).to receive(:perform_async)
-
- fork_project(@from_project, @to_user)
+ it "fork and enable CI for fork" do
+ @from_project.enable_ci
+ @to_project = fork_project(@from_project, @to_user)
+ expect(@to_project.gitlab_ci?).to be_truthy
end
end
end
@@ -100,7 +96,7 @@ describe Projects::ForkService do
end
def fork_project(from_project, user, fork_success = true, params = {})
- allow_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(fork_success)
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(fork_success)
Projects::ForkService.new(from_project, user, params).execute
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index d0f1873ee2d..2be13bb3e6a 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -14,6 +14,7 @@ require File.expand_path("../../config/environment", __FILE__)
require 'rspec/rails'
require 'shoulda/matchers'
require 'sidekiq/testing/inline'
+require 'benchmark/ips'
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
@@ -28,7 +29,11 @@ RSpec.configure do |config|
config.include LoginHelpers, type: :feature
config.include LoginHelpers, type: :request
config.include StubConfiguration
+ config.include RelativeUrl, type: feature
config.include TestEnv
+ config.include StubGitlabCalls
+ config.include StubGitlabData
+ config.include BenchmarkMatchers, benchmark: true
config.infer_spec_type_from_file_location!
config.raise_errors_for_deprecations!
@@ -38,4 +43,8 @@ RSpec.configure do |config|
end
end
+FactoryGirl::SyntaxRunner.class_eval do
+ include RSpec::Mocks::ExampleMethods
+end
+
ActiveRecord::Migration.maintain_test_schema!
diff --git a/spec/support/api_helpers.rb b/spec/support/api_helpers.rb
index f63322776d4..1b3cafb497c 100644
--- a/spec/support/api_helpers.rb
+++ b/spec/support/api_helpers.rb
@@ -28,6 +28,17 @@ module ApiHelpers
"&private_token=#{user.private_token}" : "")
end
+ def ci_api(path, user = nil)
+ "/ci/api/v1/#{path}" +
+
+ # Normalize query string
+ (path.index('?') ? '' : '?') +
+
+ # Append private_token if given a User object
+ (user.respond_to?(:private_token) ?
+ "&private_token=#{user.private_token}" : "")
+ end
+
def json_response
@_json_response ||= JSON.parse(response.body)
end
diff --git a/spec/support/filter_spec_helper.rb b/spec/support/filter_spec_helper.rb
index 25df3896291..56ce88abb48 100644
--- a/spec/support/filter_spec_helper.rb
+++ b/spec/support/filter_spec_helper.rb
@@ -58,6 +58,6 @@ module FilterSpecHelper
# Shortcut to Rails' auto-generated routes helpers, to avoid including the
# module
def urls
- Rails.application.routes.url_helpers
+ Gitlab::Application.routes.url_helpers
end
end
diff --git a/spec/support/gitlab_stubs/gitlab_ci.yml b/spec/support/gitlab_stubs/gitlab_ci.yml
new file mode 100644
index 00000000000..3482145404e
--- /dev/null
+++ b/spec/support/gitlab_stubs/gitlab_ci.yml
@@ -0,0 +1,63 @@
+image: ruby:2.1
+services:
+ - postgres
+
+before_script:
+ - gem install bundler
+ - bundle install
+ - bundle exec rake db:create
+
+variables:
+ DB_NAME: postgres
+
+types:
+ - test
+ - deploy
+ - notify
+
+rspec:
+ script: "rake spec"
+ tags:
+ - ruby
+ - postgres
+ only:
+ - branches
+
+spinach:
+ script: "rake spinach"
+ allow_failure: true
+ tags:
+ - ruby
+ - mysql
+ except:
+ - tags
+
+staging:
+ script: "cap deploy stating"
+ type: deploy
+ tags:
+ - capistrano
+ - debian
+ except:
+ - stable
+
+production:
+ type: deploy
+ script:
+ - cap deploy production
+ - cap notify
+ tags:
+ - capistrano
+ - debian
+ only:
+ - master
+ - /^deploy-.*$/
+
+dockerhub:
+ type: notify
+ script: "curl http://dockerhub/URL"
+ tags:
+ - ruby
+ - postgres
+ only:
+ - branches
diff --git a/spec/support/gitlab_stubs/project_8.json b/spec/support/gitlab_stubs/project_8.json
new file mode 100644
index 00000000000..f0a9fce859c
--- /dev/null
+++ b/spec/support/gitlab_stubs/project_8.json
@@ -0,0 +1,45 @@
+{
+ "id":8,
+ "description":"ssh access and repository management app for GitLab",
+ "default_branch":"master",
+ "public":false,
+ "visibility_level":0,
+ "ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git",
+ "http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git",
+ "web_url":"http://demo.gitlab.com/gitlab/gitlab-shell",
+ "owner": {
+ "id":4,
+ "name":"GitLab",
+ "created_at":"2012-12-21T13:03:05Z"
+ },
+ "name":"gitlab-shell",
+ "name_with_namespace":"GitLab / gitlab-shell",
+ "path":"gitlab-shell",
+ "path_with_namespace":"gitlab/gitlab-shell",
+ "issues_enabled":true,
+ "merge_requests_enabled":true,
+ "wall_enabled":false,
+ "wiki_enabled":true,
+ "snippets_enabled":false,
+ "created_at":"2013-03-20T13:28:53Z",
+ "last_activity_at":"2013-11-30T00:11:17Z",
+ "namespace":{
+ "created_at":"2012-12-21T13:03:05Z",
+ "description":"Self hosted Git management software",
+ "id":4,
+ "name":"GitLab",
+ "owner_id":1,
+ "path":"gitlab",
+ "updated_at":"2013-03-20T13:29:13Z"
+ },
+ "permissions":{
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ }
+} \ No newline at end of file
diff --git a/spec/support/gitlab_stubs/project_8_hooks.json b/spec/support/gitlab_stubs/project_8_hooks.json
new file mode 100644
index 00000000000..93d51406d63
--- /dev/null
+++ b/spec/support/gitlab_stubs/project_8_hooks.json
@@ -0,0 +1 @@
+[{}]
diff --git a/spec/support/gitlab_stubs/projects.json b/spec/support/gitlab_stubs/projects.json
new file mode 100644
index 00000000000..ca42c14c5d8
--- /dev/null
+++ b/spec/support/gitlab_stubs/projects.json
@@ -0,0 +1 @@
+[{"id":3,"description":"GitLab is open source software to collaborate on code. Create projects and repositories, manage access and do code reviews.","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlabhq.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlabhq.git","web_url":"http://demo.gitlab.com/gitlab/gitlabhq","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlabhq","name_with_namespace":"GitLab / gitlabhq","path":"gitlabhq","path_with_namespace":"gitlab/gitlabhq","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:34Z","last_activity_at":"2013-12-02T19:10:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":4,"description":"Component of GitLab CI. Web application","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-ci.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-ci.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-ci","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-ci","name_with_namespace":"GitLab / gitlab-ci","path":"gitlab-ci","path_with_namespace":"gitlab/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:06:50Z","last_activity_at":"2013-11-28T19:26:54Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":5,"description":"","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-recipes.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-recipes.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-recipes","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-recipes","name_with_namespace":"GitLab / gitlab-recipes","path":"gitlab-recipes","path_with_namespace":"gitlab/gitlab-recipes","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":true,"wiki_enabled":true,"snippets_enabled":true,"created_at":"2012-12-21T13:07:02Z","last_activity_at":"2013-12-02T13:54:10Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":8,"description":"ssh access and repository management app for GitLab","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab-shell.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab-shell.git","web_url":"http://demo.gitlab.com/gitlab/gitlab-shell","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab-shell","name_with_namespace":"GitLab / gitlab-shell","path":"gitlab-shell","path_with_namespace":"gitlab/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-03-20T13:28:53Z","last_activity_at":"2013-11-30T00:11:17Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":9,"description":null,"default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:gitlab/gitlab_git.git","http_url_to_repo":"http://demo.gitlab.com/gitlab/gitlab_git.git","web_url":"http://demo.gitlab.com/gitlab/gitlab_git","owner":{"id":4,"name":"GitLab","created_at":"2012-12-21T13:03:05Z"},"name":"gitlab_git","name_with_namespace":"GitLab / gitlab_git","path":"gitlab_git","path_with_namespace":"gitlab/gitlab_git","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-04-28T19:15:08Z","last_activity_at":"2013-12-02T13:07:13Z","namespace":{"created_at":"2012-12-21T13:03:05Z","description":"Self hosted Git management software","id":4,"name":"GitLab","owner_id":1,"path":"gitlab","updated_at":"2013-03-20T13:29:13Z"}},{"id":10,"description":"ultra lite authorization library http://randx.github.com/six/\\r\\n ","default_branch":"master","public":true,"visibility_level":20,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/six.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/six.git","web_url":"http://demo.gitlab.com/sandbox/six","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Six","name_with_namespace":"Sandbox / Six","path":"six","path_with_namespace":"sandbox/six","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:45:02Z","last_activity_at":"2013-11-29T11:30:56Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":11,"description":"Simple HTML5 Charts using the <canvas> tag ","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/charts-js.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/charts-js.git","web_url":"http://demo.gitlab.com/sandbox/charts-js","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Charts.js","name_with_namespace":"Sandbox / Charts.js","path":"charts-js","path_with_namespace":"sandbox/charts-js","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-08-01T16:47:29Z","last_activity_at":"2013-12-02T15:18:11Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}},{"id":13,"description":"","default_branch":"master","public":false,"visibility_level":0,"ssh_url_to_repo":"git@demo.gitlab.com:sandbox/afro.git","http_url_to_repo":"http://demo.gitlab.com/sandbox/afro.git","web_url":"http://demo.gitlab.com/sandbox/afro","owner":{"id":8,"name":"Sandbox","created_at":"2013-08-01T16:44:17Z"},"name":"Afro","name_with_namespace":"Sandbox / Afro","path":"afro","path_with_namespace":"sandbox/afro","issues_enabled":true,"merge_requests_enabled":true,"wall_enabled":false,"wiki_enabled":true,"snippets_enabled":false,"created_at":"2013-11-14T17:45:19Z","last_activity_at":"2013-12-02T17:41:45Z","namespace":{"created_at":"2013-08-01T16:44:17Z","description":"","id":8,"name":"Sandbox","owner_id":1,"path":"sandbox","updated_at":"2013-08-01T16:44:17Z"}}] \ No newline at end of file
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
new file mode 100644
index 00000000000..ce8dfe5ae75
--- /dev/null
+++ b/spec/support/gitlab_stubs/session.json
@@ -0,0 +1,20 @@
+{
+ "id":2,
+ "username":"jsmith",
+ "email":"test@test.com",
+ "name":"John Smith",
+ "bio":"",
+ "skype":"aertert",
+ "linkedin":"",
+ "twitter":"",
+ "theme_id":2,"color_scheme_id":2,
+ "state":"active",
+ "created_at":"2012-12-21T13:02:20Z",
+ "extern_uid":null,
+ "provider":null,
+ "is_admin":false,
+ "can_create_group":false,
+ "can_create_project":false,
+ "private_token":"Wvjy2Krpb7y8xi93owUz",
+ "access_token":"Wvjy2Krpb7y8xi93owUz"
+} \ No newline at end of file
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
new file mode 100644
index 00000000000..ce8dfe5ae75
--- /dev/null
+++ b/spec/support/gitlab_stubs/user.json
@@ -0,0 +1,20 @@
+{
+ "id":2,
+ "username":"jsmith",
+ "email":"test@test.com",
+ "name":"John Smith",
+ "bio":"",
+ "skype":"aertert",
+ "linkedin":"",
+ "twitter":"",
+ "theme_id":2,"color_scheme_id":2,
+ "state":"active",
+ "created_at":"2012-12-21T13:02:20Z",
+ "extern_uid":null,
+ "provider":null,
+ "is_admin":false,
+ "can_create_group":false,
+ "can_create_project":false,
+ "private_token":"Wvjy2Krpb7y8xi93owUz",
+ "access_token":"Wvjy2Krpb7y8xi93owUz"
+} \ No newline at end of file
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index ffe30a4246c..cd9fdc6f18e 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -44,4 +44,8 @@ module LoginHelpers
def logout_direct
page.driver.submit :delete, '/users/sign_out', {}
end
+
+ def skip_ci_admin_auth
+ allow_any_instance_of(Ci::Admin::ApplicationController).to receive_messages(authenticate_admin!: true)
+ end
end
diff --git a/spec/support/matchers/benchmark_matchers.rb b/spec/support/matchers/benchmark_matchers.rb
new file mode 100644
index 00000000000..84f655c2119
--- /dev/null
+++ b/spec/support/matchers/benchmark_matchers.rb
@@ -0,0 +1,61 @@
+module BenchmarkMatchers
+ extend RSpec::Matchers::DSL
+
+ def self.included(into)
+ into.extend(ClassMethods)
+ end
+
+ matcher :iterate_per_second do |min_iterations|
+ supports_block_expectations
+
+ match do |block|
+ @max_stddev ||= 30
+
+ @entry = benchmark(&block)
+
+ expect(@entry.ips).to be >= min_iterations
+ expect(@entry.stddev_percentage).to be <= @max_stddev
+ end
+
+ chain :with_maximum_stddev do |value|
+ @max_stddev = value
+ end
+
+ description do
+ "run at least #{min_iterations} iterations per second"
+ end
+
+ failure_message do
+ ips = @entry.ips.round(2)
+ stddev = @entry.stddev_percentage.round(2)
+
+ "expected at least #{min_iterations} iterations per second " \
+ "with a maximum stddev of #{@max_stddev}%, instead of " \
+ "#{ips} iterations per second with a stddev of #{stddev}%"
+ end
+ end
+
+ # Benchmarks the given block and returns a Benchmark::IPS::Report::Entry.
+ def benchmark(&block)
+ report = Benchmark.ips(quiet: true) do |bench|
+ bench.report do
+ instance_eval(&block)
+ end
+ end
+
+ report.entries[0]
+ end
+
+ module ClassMethods
+ # Wraps around rspec's subject method so you can write:
+ #
+ # benchmark_subject { SomeClass.some_method }
+ #
+ # instead of:
+ #
+ # subject { -> { SomeClass.some_method } }
+ def benchmark_subject(&block)
+ subject { block }
+ end
+ end
+end
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index 9df226c3af8..7500d0fdf80 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -27,6 +27,9 @@ module MarkdownMatchers
match do |actual|
expect(actual).to have_selector('img.emoji', count: 10)
+
+ image = actual.at_css('img.emoji')
+ expect(image['src'].to_s).to start_with(Gitlab.config.gitlab.url + '/assets')
end
end
diff --git a/spec/support/relative_url.rb b/spec/support/relative_url.rb
new file mode 100644
index 00000000000..72e3ccce75b
--- /dev/null
+++ b/spec/support/relative_url.rb
@@ -0,0 +1,8 @@
+# Fix route helpers in tests (e.g. root_path, ...)
+module RelativeUrl
+ extend ActiveSupport::Concern
+
+ included do
+ default_url_options[:script_name] = Rails.application.config.relative_url_root
+ end
+end
diff --git a/spec/support/services_shared_context.rb b/spec/support/services_shared_context.rb
new file mode 100644
index 00000000000..4d007ae55ee
--- /dev/null
+++ b/spec/support/services_shared_context.rb
@@ -0,0 +1,21 @@
+Service.available_services_names.each do |service|
+ shared_context service do
+ let(:dashed_service) { service.dasherize }
+ let(:service_method) { "#{service}_service".to_sym }
+ let(:service_klass) { "#{service}_service".classify.constantize }
+ let(:service_attrs_list) { service_klass.new.fields.inject([]) {|arr, hash| arr << hash[:name].to_sym } }
+ let(:service_attrs) do
+ service_attrs_list.inject({}) do |hash, k|
+ if k =~ /^(token*|.*_token|.*_key)/
+ hash.merge!(k => 'secrettoken')
+ elsif k =~ /^(.*_url|url|webhook)/
+ hash.merge!(k => "http://example.com")
+ elsif service == 'irker' && k == :recipients
+ hash.merge!(k => 'irc://irc.network.net:666/#channel')
+ else
+ hash.merge!(k => "someword")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/setup_builds_storage.rb b/spec/support/setup_builds_storage.rb
new file mode 100644
index 00000000000..a4f21e95338
--- /dev/null
+++ b/spec/support/setup_builds_storage.rb
@@ -0,0 +1,19 @@
+RSpec.configure do |config|
+ def builds_path
+ Rails.root.join('tmp/builds')
+ end
+
+ config.before(:each) do
+ FileUtils.mkdir_p(builds_path)
+ FileUtils.touch(File.join(builds_path, ".gitkeep"))
+ Settings.gitlab_ci['builds_path'] = builds_path
+ end
+
+ config.after(:suite) do
+ Dir[File.join(builds_path, '*')].each do |path|
+ next if File.basename(path) == '.gitkeep'
+
+ FileUtils.rm_rf(path)
+ end
+ end
+end
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index ef3a120d44a..f40ee862df8 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -17,8 +17,8 @@ module StubConfiguration
allow(Gitlab.config.gravatar).to receive_messages(messages)
end
- def stub_reply_by_email_setting(messages)
- allow(Gitlab.config.reply_by_email).to receive_messages(messages)
+ def stub_incoming_email_setting(messages)
+ allow(Gitlab.config.incoming_email).to receive_messages(messages)
end
private
diff --git a/spec/support/stub_gitlab_calls.rb b/spec/support/stub_gitlab_calls.rb
new file mode 100644
index 00000000000..5b3eb1bfc5f
--- /dev/null
+++ b/spec/support/stub_gitlab_calls.rb
@@ -0,0 +1,85 @@
+module StubGitlabCalls
+ def stub_gitlab_calls
+ stub_session
+ stub_user
+ stub_project_8
+ stub_project_8_hooks
+ stub_projects
+ stub_projects_owned
+ stub_ci_enable
+ end
+
+ def stub_js_gitlab_calls
+ allow_any_instance_of(Network).to receive(:projects) { project_hash_array }
+ end
+
+ def stub_ci_commit_to_return_yaml_file
+ stub_ci_commit_yaml_file(gitlab_ci_yaml)
+ end
+
+ def stub_ci_commit_yaml_file(ci_yaml)
+ allow_any_instance_of(Ci::Commit).to receive(:ci_yaml_file) { ci_yaml }
+ end
+
+ private
+
+ def gitlab_url
+ Gitlab.config.gitlab.url
+ end
+
+ def stub_session
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/session.json'))
+
+ stub_request(:post, "#{gitlab_url}api/v3/session.json").
+ with(body: "{\"email\":\"test@test.com\",\"password\":\"123456\"}",
+ headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 201, body: f, headers: { 'Content-Type'=>'application/json' })
+ end
+
+ def stub_user
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/user.json'))
+
+ stub_request(:get, "#{gitlab_url}api/v3/user?private_token=Wvjy2Krpb7y8xi93owUz").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+
+ stub_request(:get, "#{gitlab_url}api/v3/user?access_token=some_token").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+ end
+
+ def stub_project_8
+ data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8.json'))
+ allow_any_instance_of(Network).to receive(:project).and_return(JSON.parse(data))
+ end
+
+ def stub_project_8_hooks
+ data = File.read(Rails.root.join('spec/support/gitlab_stubs/project_8_hooks.json'))
+ allow_any_instance_of(Network).to receive(:project_hooks).and_return(JSON.parse(data))
+ end
+
+ def stub_projects
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
+
+ stub_request(:get, "#{gitlab_url}api/v3/projects.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: f, headers: { 'Content-Type'=>'application/json' })
+ end
+
+ def stub_projects_owned
+ stub_request(:get, "#{gitlab_url}api/v3/projects/owned.json?archived=false&ci_enabled_first=true&private_token=Wvjy2Krpb7y8xi93owUz").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: "", headers: {})
+ end
+
+ def stub_ci_enable
+ stub_request(:put, "#{gitlab_url}api/v3/projects/2/services/gitlab-ci.json?private_token=Wvjy2Krpb7y8xi93owUz").
+ with(headers: { 'Content-Type'=>'application/json' }).
+ to_return(status: 200, body: "", headers: {})
+ end
+
+ def project_hash_array
+ f = File.read(Rails.root.join('spec/support/gitlab_stubs/projects.json'))
+ JSON.parse f
+ end
+end
diff --git a/spec/support/stub_gitlab_data.rb b/spec/support/stub_gitlab_data.rb
new file mode 100644
index 00000000000..fa402f35b95
--- /dev/null
+++ b/spec/support/stub_gitlab_data.rb
@@ -0,0 +1,5 @@
+module StubGitlabData
+ def gitlab_ci_yaml
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 23f322e0a62..3be7dd4e52b 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -16,7 +16,7 @@ describe 'gitlab:app namespace rake task' do
end
def reenable_backup_sub_tasks
- %w{db repo uploads}.each do |subtask|
+ %w{db repo uploads builds}.each do |subtask|
Rake::Task["gitlab:backup:#{subtask}:create"].reenable
end
end
@@ -54,6 +54,7 @@ describe 'gitlab:app namespace rake task' do
and_return({ gitlab_version: gitlab_version })
expect(Rake::Task["gitlab:backup:db:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:backup:repo:restore"]).to receive(:invoke)
+ expect(Rake::Task["gitlab:backup:builds:restore"]).to receive(:invoke)
expect(Rake::Task["gitlab:shell:setup"]).to receive(:invoke)
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
@@ -111,18 +112,19 @@ describe 'gitlab:app namespace rake task' do
it 'should set correct permissions on the tar contents' do
tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads repositories}
+ %W{tar -tvf #{@backup_tar} db uploads repositories builds}
)
expect(exit_status).to eq(0)
expect(tar_contents).to match('db/')
expect(tar_contents).to match('uploads/')
expect(tar_contents).to match('repositories/')
- expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories)\/$/)
+ expect(tar_contents).to match('builds/')
+ expect(tar_contents).not_to match(/^.{4,9}[rwx].* (db|uploads|repositories|builds)\/$/)
end
it 'should delete temp directories' do
temp_dirs = Dir.glob(
- File.join(Gitlab.config.backup.path, '{db,repositories,uploads}')
+ File.join(Gitlab.config.backup.path, '{db,repositories,uploads,builds}')
)
expect(temp_dirs).to be_empty
@@ -157,12 +159,13 @@ describe 'gitlab:app namespace rake task' do
end
it "does not contain skipped item" do
- tar_contents, exit_status = Gitlab::Popen.popen(
- %W{tar -tvf #{@backup_tar} db uploads repositories}
+ tar_contents, _exit_status = Gitlab::Popen.popen(
+ %W{tar -tvf #{@backup_tar} db uploads repositories builds}
)
expect(tar_contents).to match('db/')
expect(tar_contents).to match('uploads/')
+ expect(tar_contents).to match('builds/')
expect(tar_contents).not_to match('repositories/')
end
@@ -173,6 +176,7 @@ describe 'gitlab:app namespace rake task' do
expect(Rake::Task["gitlab:backup:db:restore"]).to receive :invoke
expect(Rake::Task["gitlab:backup:repo:restore"]).not_to receive :invoke
+ expect(Rake::Task["gitlab:backup:builds:restore"]).to receive :invoke
expect(Rake::Task["gitlab:shell:setup"]).to receive :invoke
expect { run_rake_task('gitlab:backup:restore') }.not_to raise_error
end
diff --git a/spec/views/help/index.html.haml_spec.rb b/spec/views/help/index.html.haml_spec.rb
new file mode 100644
index 00000000000..6b07fcfc987
--- /dev/null
+++ b/spec/views/help/index.html.haml_spec.rb
@@ -0,0 +1,41 @@
+require 'rails_helper'
+
+describe 'help/index' do
+ describe 'version information' do
+ it 'is hidden from guests' do
+ stub_user(nil)
+ stub_version('8.0.2', 'abcdefg')
+ stub_helpers
+
+ render
+
+ expect(rendered).not_to match '8.0.2'
+ expect(rendered).not_to match 'abcdefg'
+ end
+
+ it 'is shown to users' do
+ stub_user
+ stub_version('8.0.2', 'abcdefg')
+ stub_helpers
+
+ render
+
+ expect(rendered).to match '8.0.2'
+ expect(rendered).to match 'abcdefg'
+ end
+ end
+
+ def stub_user(user = double)
+ allow(view).to receive(:user_signed_in?).and_return(user)
+ end
+
+ def stub_version(version, revision)
+ stub_const('Gitlab::VERSION', version)
+ stub_const('Gitlab::REVISION', revision)
+ end
+
+ def stub_helpers
+ allow(view).to receive(:markdown).and_return('')
+ allow(view).to receive(:version_status_badge).and_return('')
+ end
+end
diff --git a/spec/workers/email_receiver_worker_spec.rb b/spec/workers/email_receiver_worker_spec.rb
index e8f1bd2fa2f..65a8d7d9197 100644
--- a/spec/workers/email_receiver_worker_spec.rb
+++ b/spec/workers/email_receiver_worker_spec.rb
@@ -5,7 +5,7 @@ describe EmailReceiverWorker do
context "when reply by email is enabled" do
before do
- allow(Gitlab::ReplyByEmail).to receive(:enabled?).and_return(true)
+ allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(true)
end
it "calls the email receiver" do
@@ -33,7 +33,7 @@ describe EmailReceiverWorker do
context "when reply by email is disabled" do
before do
- allow(Gitlab::ReplyByEmail).to receive(:enabled?).and_return(false)
+ allow(Gitlab::IncomingEmail).to receive(:enabled?).and_return(false)
end
it "doesn't call the email receiver" do
diff --git a/spec/workers/fork_registration_worker_spec.rb b/spec/workers/fork_registration_worker_spec.rb
deleted file mode 100644
index cc6f574b29c..00000000000
--- a/spec/workers/fork_registration_worker_spec.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-
-require 'spec_helper'
-
-describe ForkRegistrationWorker do
- context "as a resque worker" do
- it "reponds to #perform" do
- expect(ForkRegistrationWorker.new).to respond_to(:perform)
- end
- end
-end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
new file mode 100644
index 00000000000..aa031106968
--- /dev/null
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe RepositoryForkWorker do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+
+ subject { RepositoryForkWorker.new }
+
+ describe "#perform" do
+ it "creates a new repository from a fork" do
+ expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).with(
+ project.path_with_namespace,
+ fork_project.namespace.path).
+ and_return(true)
+ expect(ProjectCacheWorker).to receive(:perform_async)
+
+ subject.perform(project.id,
+ project.path_with_namespace,
+ fork_project.namespace.path)
+ end
+
+ it "handles bad fork" do
+ expect_any_instance_of(Gitlab::Shell).to receive(:fork_repository).and_return(false)
+ subject.perform(project.id,
+ project.path_with_namespace,
+ fork_project.namespace.path)
+ end
+ end
+end