summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Bug.md44
-rw-r--r--.gitlab/issue_templates/Feature Proposal.md7
-rw-r--r--CHANGELOG111
-rw-r--r--CONTRIBUTING.md62
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--README.md3
-rw-r--r--VERSION2
-rw-r--r--app/assets/images/koding-logo.svg8
-rw-r--r--app/assets/javascripts/abuse_reports.js.es63
-rw-r--r--app/assets/javascripts/activities.js2
-rw-r--r--app/assets/javascripts/application.js10
-rw-r--r--app/assets/javascripts/awards_handler.js1
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js30
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js12
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js (renamed from app/assets/javascripts/blob/edit_blob.js)0
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es69
-rw-r--r--app/assets/javascripts/boards/components/board.js.es621
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es66
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es624
-rw-r--r--app/assets/javascripts/boards/models/label.js.es61
-rw-r--r--app/assets/javascripts/boards/models/list.js.es64
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es63
-rw-r--r--[-rwxr-xr-x]app/assets/javascripts/boards/test_utils/simulate_drag.js0
-rw-r--r--app/assets/javascripts/dispatcher.js6
-rw-r--r--app/assets/javascripts/files_comment_button.js15
-rw-r--r--app/assets/javascripts/gl_dropdown.js126
-rw-r--r--app/assets/javascripts/issuable_form.js24
-rw-r--r--app/assets/javascripts/labels_select.js32
-rw-r--r--app/assets/javascripts/lib/ace.js2
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js8
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js12
-rw-r--r--app/assets/javascripts/logo.js42
-rw-r--r--app/assets/javascripts/member_expiration_date.js32
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es64
-rw-r--r--app/assets/javascripts/project.js13
-rw-r--r--app/assets/javascripts/project_members.js3
-rw-r--r--app/assets/javascripts/protected_branch_access_dropdown.js.es68
-rw-r--r--app/assets/javascripts/protected_branch_create.js.es64
-rw-r--r--app/assets/javascripts/protected_branch_edit.js.es63
-rw-r--r--app/assets/javascripts/right_sidebar.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js18
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js12
-rw-r--r--app/assets/javascripts/user.js4
-rw-r--r--app/assets/javascripts/users/calendar.js51
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/common.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss21
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/forms.scss1
-rw-r--r--app/assets/stylesheets/framework/header.scss32
-rw-r--r--app/assets/stylesheets/framework/logo.scss118
-rw-r--r--app/assets/stylesheets/framework/mixins.scss46
-rw-r--r--app/assets/stylesheets/framework/modal.scss1
-rw-r--r--app/assets/stylesheets/framework/nav.scss30
-rw-r--r--app/assets/stylesheets/framework/selects.scss3
-rw-r--r--app/assets/stylesheets/pages/admin.scss4
-rw-r--r--app/assets/stylesheets/pages/boards.scss86
-rw-r--r--app/assets/stylesheets/pages/builds.scss7
-rw-r--r--app/assets/stylesheets/pages/commit.scss17
-rw-r--r--app/assets/stylesheets/pages/events.scss5
-rw-r--r--app/assets/stylesheets/pages/import.scss19
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss19
-rw-r--r--app/assets/stylesheets/pages/notes.scss16
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss88
-rw-r--r--app/assets/stylesheets/pages/projects.scss61
-rw-r--r--app/assets/stylesheets/pages/status.scss9
-rw-r--r--app/assets/stylesheets/pages/tree.scss10
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/impersonations_controller.rb2
-rw-r--r--app/controllers/admin/system_info_controller.rb8
-rw-r--r--app/controllers/application_controller.rb29
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb10
-rw-r--r--app/controllers/dashboard/todos_controller.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/import/gitorious_controller.rb47
-rw-r--r--app/controllers/koding_controller.rb15
-rw-r--r--app/controllers/projects/artifacts_controller.rb44
-rw-r--r--app/controllers/projects/avatars_controller.rb2
-rw-r--r--app/controllers/projects/boards/issues_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb7
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/issues_controller.rb8
-rw-r--r--app/controllers/projects/merge_requests_controller.rb19
-rw-r--r--app/controllers/projects/project_members_controller.rb9
-rw-r--r--app/controllers/projects_controller.rb2
-rw-r--r--app/finders/move_to_project_finder.rb6
-rw-r--r--app/finders/todos_finder.rb6
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb8
-rw-r--r--app/helpers/ci_status_helper.rb13
-rw-r--r--app/helpers/gitlab_routing_helper.rb16
-rw-r--r--app/helpers/issuables_helper.rb9
-rw-r--r--app/helpers/lfs_helper.rb4
-rw-r--r--app/helpers/merge_requests_helper.rb2
-rw-r--r--app/helpers/nav_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb87
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sentry_helper.rb27
-rw-r--r--app/helpers/time_helper.rb17
-rw-r--r--app/models/ability.rb50
-rw-r--r--app/models/application_setting.rb8
-rw-r--r--app/models/ci/build.rb3
-rw-r--r--app/models/ci/pipeline.rb18
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/commit_status.rb17
-rw-r--r--app/models/concerns/awardable.rb12
-rw-r--r--app/models/concerns/expirable.rb15
-rw-r--r--app/models/concerns/has_status.rb (renamed from app/models/concerns/statuseable.rb)17
-rw-r--r--app/models/concerns/issuable.rb29
-rw-r--r--app/models/concerns/note_on_diff.rb8
-rw-r--r--app/models/concerns/sortable.rb14
-rw-r--r--app/models/concerns/spammable.rb2
-rw-r--r--app/models/concerns/taskable.rb4
-rw-r--r--app/models/diff_note.rb11
-rw-r--r--app/models/discussion.rb1
-rw-r--r--app/models/group.rb24
-rw-r--r--app/models/legacy_diff_note.rb4
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/members/project_member.rb10
-rw-r--r--app/models/merge_request.rb104
-rw-r--r--app/models/merge_request_diff.rb170
-rw-r--r--app/models/note.rb6
-rw-r--r--app/models/project.rb36
-rw-r--r--app/models/project_group_link.rb4
-rw-r--r--app/models/project_team.rb13
-rw-r--r--app/models/repository.rb12
-rw-r--r--app/models/todo.rb19
-rw-r--r--app/models/user.rb4
-rw-r--r--app/services/boards/issues/list_service.rb7
-rw-r--r--app/services/boards/lists/create_service.rb9
-rw-r--r--app/services/ci/process_pipeline_service.rb2
-rw-r--r--app/services/ci/web_hook_service.rb35
-rw-r--r--app/services/issuable_base_service.rb20
-rw-r--r--app/services/members/authorized_destroy_service.rb19
-rw-r--r--app/services/members/destroy_service.rb7
-rw-r--r--app/services/merge_requests/resolve_service.rb21
-rw-r--r--app/services/merge_requests/update_service.rb4
-rw-r--r--app/services/system_note_service.rb10
-rw-r--r--app/services/todo_service.rb3
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/application_settings/_form.html.haml19
-rw-r--r--app/views/admin/background_jobs/_head.html.haml46
-rw-r--r--app/views/admin/builds/_build.html.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml54
-rw-r--r--app/views/admin/projects/show.html.haml6
-rw-r--r--app/views/admin/system_info/show.html.haml12
-rw-r--r--app/views/ci/lints/show.html.haml3
-rw-r--r--app/views/dashboard/todos/index.html.haml19
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml9
-rw-r--r--app/views/groups/group_members/index.html.haml2
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/import/gitorious/status.html.haml54
-rw-r--r--app/views/koding/index.html.haml6
-rw-r--r--app/views/layouts/koding.html.haml5
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml5
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml9
-rw-r--r--app/views/projects/blob/new.html.haml9
-rw-r--r--app/views/projects/boards/components/_board.html.haml6
-rw-r--r--app/views/projects/boards/components/_card.html.haml1
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml46
-rw-r--r--app/views/projects/buttons/_koding.html.haml7
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml5
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml26
-rw-r--r--app/views/projects/commit/_change.html.haml4
-rw-r--r--app/views/projects/commit/_ci_stage.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml5
-rw-r--r--app/views/projects/commit/_pipelines_list.haml5
-rw-r--r--app/views/projects/commits/_head.html.haml5
-rw-r--r--app/views/projects/edit.html.haml10
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml4
-rw-r--r--app/views/projects/graphs/_head.html.haml32
-rw-r--r--app/views/projects/group_links/index.html.haml11
-rw-r--r--app/views/projects/issues/_head.html.haml54
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_diffs.html.haml1
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml31
-rw-r--r--app/views/projects/new.html.haml5
-rw-r--r--app/views/projects/notes/_hints.html.haml4
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/projects/pipelines/_head.html.haml36
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml5
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml9
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml16
-rw-r--r--app/views/projects/releases/edit.html.haml16
-rw-r--r--app/views/projects/repositories/_download_archive.html.haml37
-rw-r--r--app/views/projects/show.html.haml6
-rw-r--r--app/views/projects/tags/_download.html.haml14
-rw-r--r--app/views/projects/tags/_tag.html.haml3
-rw-r--r--app/views/projects/tags/show.html.haml3
-rw-r--r--app/views/projects/tree/_tree_content.html.haml11
-rw-r--r--app/views/projects/tree/show.html.haml3
-rw-r--r--app/views/projects/wikis/_nav.html.haml22
-rw-r--r--app/views/shared/_logo.svg16
-rw-r--r--app/views/shared/_nav_scroll.html.haml4
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/icons/_icon_play.svg1
-rw-r--r--app/views/shared/icons/_icon_status_created.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml21
-rw-r--r--app/views/shared/issuable/_form.html.haml17
-rw-r--r--app/views/shared/issuable/_label_page_default.html.haml5
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml20
-rw-r--r--app/views/shared/snippets/_form.html.haml9
-rw-r--r--app/workers/emails_on_push_worker.rb4
-rw-r--r--app/workers/remove_expired_group_links_worker.rb7
-rw-r--r--app/workers/remove_expired_members_worker.rb13
-rw-r--r--app/workers/repository_import_worker.rb2
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/1_settings.rb8
-rw-r--r--config/initializers/ar_monkey_patch.rb57
-rw-r--r--config/routes.rb19
-rw-r--r--db/fixtures/development/14_pipelines.rb (renamed from db/fixtures/development/14_builds.rb)99
-rw-r--r--db/migrate/20160707104333_add_lock_to_issuables.rb18
-rw-r--r--db/migrate/20160725104020_merge_request_diff_remove_uniq.rb21
-rw-r--r--db/migrate/20160725104452_merge_request_diff_add_index.rb17
-rw-r--r--db/migrate/20160801163421_add_expires_at_to_member.rb29
-rw-r--r--db/migrate/20160817133006_add_koding_to_application_settings.rb10
-rw-r--r--db/migrate/20160818205718_add_expires_at_to_project_group_links.rb29
-rw-r--r--db/migrate/20160819221631_add_index_to_note_discussion_id.rb14
-rw-r--r--db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb12
-rw-r--r--db/migrate/20160823081327_change_merge_error_to_text.rb10
-rw-r--r--db/migrate/20160823213309_add_lfs_enabled_to_projects.rb29
-rw-r--r--db/migrate/20160824103857_drop_unused_ci_tables.rb11
-rw-r--r--db/migrate/20160827011312_ensure_lock_version_has_no_default.rb16
-rw-r--r--db/migrate/20160830232601_change_lock_version_not_null.rb13
-rw-r--r--db/schema.rb59
-rw-r--r--doc/README.md2
-rw-r--r--doc/administration/integration/koding.md242
-rw-r--r--doc/api/commits.md14
-rw-r--r--doc/api/issues.md39
-rw-r--r--doc/api/members.md5
-rw-r--r--doc/api/merge_requests.md131
-rw-r--r--doc/api/project_snippets.md3
-rw-r--r--doc/api/projects.md113
-rw-r--r--doc/ci/pipelines.md4
-rw-r--r--doc/ci/yaml/README.md21
-rw-r--r--doc/development/doc_styleguide.md26
-rw-r--r--doc/development/newlines_styleguide.md2
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/integration/README.md1
-rw-r--r--doc/integration/bitbucket.md215
-rw-r--r--doc/integration/img/bitbucket_oauth_keys.pngbin0 -> 12073 bytes
-rw-r--r--doc/integration/img/bitbucket_oauth_settings_page.pngbin0 -> 82818 bytes
-rw-r--r--doc/integration/omniauth.md4
-rw-r--r--doc/update/8.10-to-8.11.md42
-rw-r--r--doc/user/account/security.md3
-rw-r--r--doc/user/account/two_factor_authentication.md68
-rw-r--r--doc/user/markdown.md20
-rw-r--r--doc/user/project/img/issue_board.pngbin0 -> 275093 bytes
-rw-r--r--doc/user/project/img/issue_board_add_list.pngbin0 -> 22391 bytes
-rw-r--r--doc/user/project/img/issue_board_search_backlog.pngbin0 -> 25948 bytes
-rw-r--r--doc/user/project/img/issue_board_system_notes.pngbin0 -> 20637 bytes
-rw-r--r--doc/user/project/img/issue_board_welcome_message.pngbin0 -> 78694 bytes
-rw-r--r--doc/user/project/img/koding_build-in-progress.pngbin0 -> 70949 bytes
-rw-r--r--doc/user/project/img/koding_build-logs.pngbin0 -> 263623 bytes
-rw-r--r--doc/user/project/img/koding_build-success.pngbin0 -> 304666 bytes
-rw-r--r--doc/user/project/img/koding_commit-koding.yml.pngbin0 -> 302703 bytes
-rw-r--r--doc/user/project/img/koding_different-stack-on-mr-try.pngbin0 -> 333649 bytes
-rw-r--r--doc/user/project/img/koding_edit-on-ide.pngbin0 -> 330880 bytes
-rw-r--r--doc/user/project/img/koding_enable-koding.pngbin0 -> 73499 bytes
-rw-r--r--doc/user/project/img/koding_landing.pngbin0 -> 268455 bytes
-rw-r--r--doc/user/project/img/koding_open-gitlab-from-koding.pngbin0 -> 32559 bytes
-rw-r--r--doc/user/project/img/koding_run-in-ide.pngbin0 -> 65465 bytes
-rw-r--r--doc/user/project/img/koding_run-mr-in-ide.pngbin0 -> 339759 bytes
-rw-r--r--doc/user/project/img/koding_set-up-ide.pngbin0 -> 207481 bytes
-rw-r--r--doc/user/project/img/koding_stack-import.pngbin0 -> 500352 bytes
-rw-r--r--doc/user/project/img/koding_start-build.pngbin0 -> 105253 bytes
-rw-r--r--doc/user/project/img/protected_branches_devs_can_push.pngbin23976 -> 19312 bytes
-rw-r--r--doc/user/project/img/protected_branches_list.pngbin16817 -> 16223 bytes
-rw-r--r--doc/user/project/img/protected_branches_page.pngbin0 -> 17839 bytes
-rw-r--r--doc/user/project/issue_board.md187
-rw-r--r--doc/user/project/koding.md128
-rw-r--r--doc/user/project/merge_requests/resolve_conflicts.md1
-rw-r--r--doc/user/project/protected_branches.md81
-rw-r--r--doc/user/project/slash_commands.md (renamed from doc/workflow/slash_commands.md)2
-rw-r--r--doc/workflow/README.md3
-rw-r--r--doc/workflow/gitlab_flow.md4
-rw-r--r--doc/workflow/merge_requests.md29
-rw-r--r--doc/workflow/merge_requests/versions.pngbin0 -> 100566 bytes
-rw-r--r--doc/workflow/project_features.md10
-rw-r--r--doc/workflow/share_projects_with_other_groups.md18
-rw-r--r--features/project/commits/branches.feature1
-rw-r--r--features/project/merge_requests.feature4
-rw-r--r--features/steps/dashboard/new_project.rb1
-rw-r--r--features/steps/group/members.rb4
-rw-r--r--features/steps/project/commits/branches.rb5
-rw-r--r--features/steps/project/merge_requests.rb6
-rw-r--r--features/steps/project/source/browse_files.rb8
-rw-r--r--features/steps/project/team_management.rb4
-rw-r--r--features/steps/shared/issuable.rb2
-rw-r--r--lib/api/api.rb13
-rw-r--r--lib/api/award_emoji.rb6
-rw-r--r--lib/api/commit_statuses.rb2
-rw-r--r--lib/api/entities.rb41
-rw-r--r--lib/api/helpers.rb32
-rw-r--r--lib/api/internal.rb25
-rw-r--r--lib/api/issues.rb20
-rw-r--r--lib/api/members.rb9
-rw-r--r--lib/api/merge_request_diffs.rb45
-rw-r--r--lib/api/project_hooks.rb2
-rw-r--r--lib/api/projects.rb15
-rw-r--r--lib/backup/repository.rb2
-rw-r--r--lib/ci/api/api.rb12
-rw-r--r--lib/extracts_path.rb3
-rw-r--r--lib/gitlab/badge/coverage/report.rb5
-rw-r--r--lib/gitlab/ci/config/node/hidden.rb (renamed from lib/gitlab/ci/config/node/hidden_job.rb)3
-rw-r--r--lib/gitlab/ci/config/node/jobs.rb2
-rw-r--r--lib/gitlab/conflict/file.rb11
-rw-r--r--lib/gitlab/conflict/parser.rb9
-rw-r--r--lib/gitlab/contributions_calendar.rb1
-rw-r--r--lib/gitlab/current_settings.rb3
-rw-r--r--lib/gitlab/diff/file_collection/merge_request_diff.rb (renamed from lib/gitlab/diff/file_collection/merge_request.rb)16
-rw-r--r--lib/gitlab/diff/position.rb18
-rw-r--r--lib/gitlab/email/handler.rb3
-rw-r--r--lib/gitlab/github_import/importer.rb87
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb4
-rw-r--r--lib/gitlab/gitorious_import.rb5
-rw-r--r--lib/gitlab/gitorious_import/client.rb29
-rw-r--r--lib/gitlab/gitorious_import/project_creator.rb27
-rw-r--r--lib/gitlab/gitorious_import/repository.rb35
-rw-r--r--lib/gitlab/import_sources.rb13
-rw-r--r--lib/gitlab/metrics/rack_middleware.rb22
-rw-r--r--lib/gitlab/middleware/rails_queue_duration.rb2
-rw-r--r--lib/gitlab/url_builder.rb2
-rw-r--r--lib/gitlab/utils.rb2
-rw-r--r--spec/controllers/admin/impersonations_controller_spec.rb2
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb50
-rw-r--r--spec/controllers/import/gitorious_controller_spec.rb69
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb39
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb29
-rw-r--r--spec/factories/project_hooks.rb1
-rw-r--r--spec/features/admin/admin_system_info_spec.rb47
-rw-r--r--spec/features/boards/boards_spec.rb77
-rw-r--r--spec/features/issues/award_emoji_spec.rb1
-rw-r--r--spec/features/issues/new_branch_button_spec.rb2
-rw-r--r--spec/features/issues_spec.rb13
-rw-r--r--spec/features/merge_requests/conflicts_spec.rb3
-rw-r--r--spec/features/merge_requests/diff_notes_spec.rb238
-rw-r--r--spec/features/merge_requests/edit_mr_spec.rb11
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb37
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb2
-rw-r--r--spec/features/projects/badges/coverage_spec.rb41
-rw-r--r--spec/features/projects/branches/delete_spec.rb24
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb40
-rw-r--r--spec/features/projects/branches_spec.rb2
-rw-r--r--spec/features/projects/builds_spec.rb (renamed from spec/features/builds_spec.rb)0
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb31
-rw-r--r--spec/features/projects/files/download_buttons_spec.rb41
-rw-r--r--spec/features/projects/group_links_spec.rb32
-rw-r--r--spec/features/projects/main/download_buttons_spec.rb40
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb45
-rw-r--r--spec/features/projects/tags/download_buttons_spec.rb41
-rw-r--r--spec/features/projects_spec.rb29
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb71
-rw-r--r--spec/features/protected_branches_spec.rb71
-rw-r--r--spec/features/search_spec.rb10
-rw-r--r--spec/features/security/dashboard_access_spec.rb14
-rw-r--r--spec/features/task_lists_spec.rb266
-rw-r--r--spec/features/todos/todos_sorting_spec.rb67
-rw-r--r--spec/features/todos/todos_spec.rb21
-rw-r--r--spec/finders/move_to_project_finder_spec.rb22
-rw-r--r--spec/finders/todos_finder_spec.rb70
-rw-r--r--spec/fixtures/api/schemas/issue.json40
-rw-r--r--spec/helpers/issuables_helper_spec.rb16
-rw-r--r--spec/helpers/projects_helper_spec.rb38
-rw-r--r--spec/helpers/time_helper_spec.rb16
-rw-r--r--spec/javascripts/awards_handler_spec.js32
-rw-r--r--spec/javascripts/boards/list_spec.js.es69
-rw-r--r--spec/javascripts/datetime_utility_spec.js.coffee19
-rw-r--r--spec/javascripts/fixtures/awards_handler.html.haml2
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml16
-rw-r--r--spec/javascripts/fixtures/issue_sidebar_label.html.haml16
-rw-r--r--spec/javascripts/fixtures/projects.json2
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es6119
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es689
-rw-r--r--spec/javascripts/search_autocomplete_spec.js8
-rw-r--r--spec/lib/extracts_path_spec.rb29
-rw-r--r--spec/lib/gitlab/badge/coverage/report_spec.rb67
-rw-r--r--spec/lib/gitlab/ci/config/node/hidden_spec.rb (renamed from spec/lib/gitlab/ci/config/node/hidden_job_spec.rb)17
-rw-r--r--spec/lib/gitlab/ci/config/node/jobs_spec.rb2
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb5
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb42
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb132
-rw-r--r--spec/lib/gitlab/github_import/pull_request_formatter_spec.rb11
-rw-r--r--spec/lib/gitlab/gitorious_import/project_creator_spec.rb26
-rw-r--r--spec/lib/gitlab/metrics/rack_middleware_spec.rb21
-rw-r--r--spec/lib/gitlab/middleware/rails_queue_duration_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb14
-rw-r--r--spec/models/ability_spec.rb77
-rw-r--r--spec/models/broadcast_message_spec.rb2
-rw-r--r--spec/models/build_spec.rb8
-rw-r--r--spec/models/ci/pipeline_spec.rb44
-rw-r--r--spec/models/concerns/has_status_spec.rb (renamed from spec/models/concerns/statuseable_spec.rb)6
-rw-r--r--spec/models/member_spec.rb14
-rw-r--r--spec/models/merge_request_diff_spec.rb21
-rw-r--r--spec/models/merge_request_spec.rb141
-rw-r--r--spec/models/network/graph_spec.rb12
-rw-r--r--spec/models/note_spec.rb2
-rw-r--r--spec/models/project_spec.rb83
-rw-r--r--spec/models/repository_spec.rb8
-rw-r--r--spec/models/user_spec.rb10
-rw-r--r--spec/requests/api/api_helpers_spec.rb27
-rw-r--r--spec/requests/api/award_emoji_spec.rb18
-rw-r--r--spec/requests/api/builds_spec.rb20
-rw-r--r--spec/requests/api/commits_spec.rb4
-rw-r--r--spec/requests/api/internal_spec.rb62
-rw-r--r--spec/requests/api/issues_spec.rb26
-rw-r--r--spec/requests/api/members_spec.rb6
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb49
-rw-r--r--spec/requests/api/merge_requests_spec.rb1
-rw-r--r--spec/requests/api/notes_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb8
-rw-r--r--spec/requests/api/project_snippets_spec.rb1
-rw-r--r--spec/requests/api/projects_spec.rb28
-rw-r--r--spec/requests/lfs_http_spec.rb107
-rw-r--r--spec/requests/projects/artifacts_controller_spec.rb117
-rw-r--r--spec/routing/routing_spec.rb13
-rw-r--r--spec/services/boards/issues/list_service_spec.rb8
-rw-r--r--spec/services/boards/lists/create_service_spec.rb11
-rw-r--r--spec/services/ci/image_for_build_service_spec.rb2
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb46
-rw-r--r--spec/services/merge_requests/merge_request_diff_cache_service_spec.rb2
-rw-r--r--spec/services/merge_requests/resolve_service_spec.rb87
-rw-r--r--spec/services/system_note_service_spec.rb10
-rw-r--r--spec/services/todo_service_spec.rb21
-rw-r--r--spec/simplecov_env.rb3
-rw-r--r--spec/spec_helper.rb7
-rw-r--r--spec/support/taskable_shared_examples.rb63
-rw-r--r--spec/support/test_env.rb50
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb53
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb44
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb36
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb24
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb58
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/Chart.js0
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/autosize.js0
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/jquery.scrollTo.js0
456 files changed, 7372 insertions, 2365 deletions
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
new file mode 100644
index 00000000000..b676916fdf4
--- /dev/null
+++ b/.gitlab/issue_templates/Bug.md
@@ -0,0 +1,44 @@
+### Summary
+
+(Summarize the bug encountered concisely)
+
+### Steps to reproduce
+
+(How one can reproduce the issue - this is very important)
+
+### Expected behavior
+
+(What you should see instead)
+
+### Actual behaviour
+
+(What actually happens)
+
+### Relevant logs and/or screenshots
+
+(Paste any relevant logs - please use code blocks (```) to format console output,
+logs, and code as it's very hard to read otherwise.)
+
+### Output of checks
+
+#### Results of GitLab application Check
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:check SANITIZE=true`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true`)
+
+(we will only investigate if the tests are passing)
+
+#### Results of GitLab environment info
+
+(For installations with omnibus-gitlab package run and paste the output of:
+`sudo gitlab-rake gitlab:env:info`)
+
+(For installations from source run and paste the output of:
+`sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production`)
+
+### Possible fixes
+
+(If you can, link to the line of code that might be responsible for the problem)
diff --git a/.gitlab/issue_templates/Feature Proposal.md b/.gitlab/issue_templates/Feature Proposal.md
new file mode 100644
index 00000000000..ea895ee6275
--- /dev/null
+++ b/.gitlab/issue_templates/Feature Proposal.md
@@ -0,0 +1,7 @@
+### Description
+
+(Include problem, use cases, benefits, and/or goals)
+
+### Proposal
+
+### Links / references
diff --git a/CHANGELOG b/CHANGELOG
index fec7c56c32d..7941a29d4ed 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,12 +1,97 @@
Please view this file on the master branch, on stable branches it's out of date.
+v 8.12.0 (unreleased)
+ - Make push events have equal vertical spacing.
+ - Add two-factor recovery endpoint to internal API !5510
+ - Remove vendor prefixes for linear-gradient CSS (ClemMakesApps)
+ - Add font color contrast to external label in admin area (ClemMakesApps)
+ - Change logo animation to CSS (ClemMakesApps)
+ - Change merge_error column from string to text type
+ - Reduce contributions calendar data payload (ClemMakesApps)
+ - Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
+ - Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
+ - Shorten task status phrase (ClemMakesApps)
+ - Add hover color to emoji icon (ClemMakesApps)
+ - Fix branches page dropdown sort alignment (ClemMakesApps)
+ - Add white background for no readme container (ClemMakesApps)
+ - Optimistic locking for Issues and Merge Requests (title and description overriding prevention)
+ - Add `wiki_page_events` to project hook APIs (Ben Boeckel)
+ - Remove Gitorious import
+ - Fix inconsistent background color for filter input field (ClemMakesApps)
+ - Add Sentry logging to API calls
+ - Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+ - Remove unused mixins (ClemMakesApps)
+ - Add search to all issue board lists
+ - Fix groups sort dropdown alignment (ClemMakesApps)
+ - Add horizontal scrolling to all sub-navs on mobile viewports (ClemMakesApps)
+ - Fix markdown help references (ClemMakesApps)
+ - Add last commit time to repo view (ClemMakesApps)
+ - Added project specific enable/disable setting for LFS !5997
+ - Added tests for diff notes
+ - Add a button to download latest successful artifacts for branches and tags !5142
+ - Remove redundant pipeline tooltips (ClemMakesApps)
+ - Add delimiter to project stars and forks count (ClemMakesApps)
+ - Fix badge count alignment (ClemMakesApps)
+ - Fix branch title trailing space on hover (ClemMakesApps)
+ - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
+ - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
+ - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
+ - Update merge_requests.md with a simpler way to check out a merge request. !5944
+ - Fix button missing type (ClemMakesApps)
+ - Move to project dropdown with infinite scroll for better performance
+ - Fix leaking of submit buttons outside the width of a main container !18731 (originally by @pavelloz)
+ - Load branches asynchronously in Cherry Pick and Revert dialogs.
+ - Add merge request versions !5467
+ - Change using size to use count and caching it for number of group members. !5935
+ - Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck)
+ - Reduce number of database queries on builds tab
+ - Capitalize mentioned issue timeline notes (ClemMakesApps)
+ - Use the default branch for displaying the project icon instead of master !5792 (Hannes Rosenögger)
+ - Adds response mime type to transaction metric action when it's not HTML
+ - Fix hover leading space bug in pipeline graph !5980
+ - User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
+
+v 8.11.4 (unreleased)
+ - Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
+ - Creating an issue through our API now emails label subscribers !5720
+ - Fix resolving conflicts on forks
+ - Fix diff commenting on merge requests created prior to 8.10
+
+v 8.11.4 (unreleased)
+ - Fix issue boards leak private label names and descriptions
+
+v 8.11.3 (unreleased)
+v 8.11.3
+ - Do not enforce using hash with hidden key in CI configuration. !6079
+ - Allow system info page to handle case where info is unavailable
+ - Label list shows all issues (opened or closed) with that label
+ - Don't show resolve conflicts link before MR status is updated
+ - Fix "Wiki" link not appearing in navigation for projects with external wiki
+ - Fix IE11 fork button bug !598
+ - Don't prevent viewing the MR when git refs for conflicts can't be found on disk
+ - Fix external issue tracker "Issues" link leading to 404s
+
+v 8.11.2
+ - Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
+ - Use gitlab-workhorse 0.7.11 !5983
+ - Does not halt the GitHub import process when an error occurs. !5763
+ - Fix file links on project page when default view is Files !5933
+ - Fixed enter key in search input not working !5888
+
+v 8.11.1
+ - Pulled due to packaging error.
+
v 8.11.0 (unreleased)
+ - Fix pipelines tab layout regression (brycepj)
+v 8.11.0
+ - Use test coverage value from the latest successful pipeline in badge. !5862
- Add test coverage report badge. !5708
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
+ - Add Koding (online IDE) integration
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
- - Add delimiter to project stars and forks count (ClemMakesApps)
- Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
+ - Fix adding line comments on the initial commit to a repo !5900
- Fix the title of the toggle dropdown button. !5515 (herminiotorres)
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
- Update to Ruby 2.3.1. !4948
@@ -18,8 +103,8 @@ v 8.11.0 (unreleased)
- API: Endpoints for enabling and disabling deploy keys
- API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
- Use long options for curl examples in documentation !5703 (winniehell)
+ - Added tooltip listing label names to the labels value in the collapsed issuable sidebar
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
- - Fix badge count alignment (ClemMakesApps)
- GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
- Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
- Allow naming U2F devices !5833
@@ -27,6 +112,7 @@ v 8.11.0 (unreleased)
- Fix CI status icon link underline (ClemMakesApps)
- The Repository class is now instrumented
- Fix commit mention font inconsistency (ClemMakesApps)
+ - Do not escape URI when extracting path !5878 (winniehell)
- Fix filter label tooltip HTML rendering (ClemMakesApps)
- Cache the commit author in RequestStore to avoid extra lookups in PostReceive
- Expand commit message width in repo view (ClemMakesApps)
@@ -36,9 +122,11 @@ v 8.11.0 (unreleased)
- API: Add deployment endpoints
- API: Add Play endpoint on Builds
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
+ - Show wall clock time when showing a pipeline. !5734
- Show member roles to all users on members page
- Project.visible_to_user is instrumented again
- Fix awardable button mutuality loading spinners (ClemMakesApps)
+ - Sort todos by date and priority
- Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
- Optimize maximum user access level lookup in loading of notes
- Send notification emails to users newly mentioned in issue and MR edits !5800
@@ -57,7 +145,6 @@ v 8.11.0 (unreleased)
- Enforce 2FA restrictions on API authentication endpoints !5820
- Limit git rev-list output count to one in forced push check
- Show deployment status on merge requests with external URLs
- - Fix branch title trailing space on hover (ClemMakesApps)
- Clean up unused routes (Josef Strzibny)
- Fix issue on empty project to allow developers to only push to protected branches if given permission
- API: Add enpoints for pipelines
@@ -82,7 +169,6 @@ v 8.11.0 (unreleased)
- Add archived badge to project list !5798
- Add simple identifier to public SSH keys (muteor)
- Admin page now references docs instead of a specific file !5600 (AnAverageHuman)
- - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363
- Fix filter input alignment (ClemMakesApps)
- Include old revision in merge request update hooks (Ben Boeckel)
- Add build event color in HipChat messages (David Eisner)
@@ -95,8 +181,6 @@ v 8.11.0 (unreleased)
- Remove `search_id` of labels dropdown filter to fix 'Missleading URI for labels in Merge Requests and Issues view'. !5368 (Scott Le)
- Load project invited groups and members eagerly in `ProjectTeam#fetch_members`
- Add pipeline events hook
- - Award emoji tooltips containing more than 10 usernames are now truncated !4780 (jlogandavison)
- - Fix duplicate "me" in award emoji tooltip !5218 (jlogandavison)
- Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
- Make branches sortable without push permission !5462 (winniehell)
@@ -108,14 +192,12 @@ v 8.11.0 (unreleased)
- Fix search for notes which belongs to deleted objects
- Allow Akismet to be trained by submitting issues as spam or ham !5538
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
- - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem
- Improve OAuth2 client documentation (muteor)
- Fix diff comments inverted toggle bug (ClemMakesApps)
- Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
- Profile requests when a header is passed
- - Fix button missing type (ClemMakesApps)
- Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
- Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
- Add commit stats in commit api. !5517 (dixpac)
@@ -129,6 +211,7 @@ v 8.11.0 (unreleased)
- Change requests_profiles resource constraint to catch virtually any file
- Bump gitlab_git to lazy load compare commits
- Reduce number of queries made for merge_requests/:id/diffs
+ - Add the option to set the expiration date for the project membership when giving a user access to a project. !5599 (Adam Niedzielski)
- Sensible state specific default sort order for issues and merge requests !5453 (tomb0y)
- Fix bug where destroying a namespace would not always destroy projects
- Fix RequestProfiler::Middleware error when code is reloaded in development
@@ -155,6 +238,10 @@ v 8.11.0 (unreleased)
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
+v 8.10.7
+ - Upgrade Hamlit to 2.6.1. !5873
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.10.6
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Restore "Largest repository" sort option on Admin > Projects page. !5797
@@ -183,8 +270,6 @@ v 8.10.3
- Trim extra displayed carriage returns in diffs and files with CRLFs. !5588
- Fix label already exist error message in the right sidebar.
-v 8.10.3 (unreleased)
-
v 8.10.2
- User can now search branches by name. !5144
- Page is now properly rendered after committing the first file and creating the first branch. !5399
@@ -378,6 +463,9 @@ v 8.10.0
- Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page
+v 8.9.8
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.9.7
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Require administrator privileges to perform a project import.
@@ -647,6 +735,9 @@ v 8.9.0
- Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation
+v 8.8.9
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.8.8
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 4f10f90a82d..c77dcd96a7d 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -129,7 +129,7 @@ request that potentially fixes it.
### Feature proposals
-To create a feature proposal for CE and CI, open an issue on the
+To create a feature proposal for CE, open an issue on the
[issue tracker of CE][ce-tracker].
For feature proposals for EE, open an issue on the
@@ -144,16 +144,7 @@ code snippet right after your description in a new line: `~"feature proposal"`.
Please keep feature proposals as small and simple as possible, complex ones
might be edited to make them small and simple.
-You are encouraged to use the template below for feature proposals.
-
-```
-## Description
-Include problem, use cases, benefits, and/or goals
-
-## Proposal
-
-## Links / references
-```
+Please submit Feature Proposals using the ['Feature Proposal' issue template](.gitlab/issue_templates/Feature Proposal.md) provided on the issue tracker.
For changes in the interface, it can be helpful to create a mockup first.
If you want to create something yourself, consider opening an issue first to
@@ -166,55 +157,11 @@ submitting your own, there's a good chance somebody else had the same issue or
feature proposal. Show your support with an award emoji and/or join the
discussion.
-Please submit bugs using the following template in the issue description area.
+Please submit bugs using the ['Bug' issue template](.gitlab/issue_templates/Bug.md) provided on the issue tracker.
The text in the parenthesis is there to help you with what to include. Omit it
when submitting the actual issue. You can copy-paste it and then edit as you
see fit.
-```
-## Summary
-
-(Summarize your issue in one sentence - what goes wrong, what did you expect to happen)
-
-## Steps to reproduce
-
-(How one can reproduce the issue - this is very important)
-
-## Expected behavior
-
-(What you should see instead)
-
-## Relevant logs and/or screenshots
-
-(Paste any relevant logs - please use code blocks (```) to format console output,
-logs, and code as it's very hard to read otherwise.)
-
-## Output of checks
-
-### Results of GitLab Application Check
-
-(For installations with omnibus-gitlab package run and paste the output of:
-sudo gitlab-rake gitlab:check SANITIZE=true)
-
-(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production SANITIZE=true)
-
-(we will only investigate if the tests are passing)
-
-### Results of GitLab Environment Info
-
-(For installations with omnibus-gitlab package run and paste the output of:
-sudo gitlab-rake gitlab:env:info)
-
-(For installations from source run and paste the output of:
-sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production)
-
-## Possible fixes
-
-(If you can, link to the line of code that might be responsible for the problem)
-
-```
-
### Issue weight
Issue weight allows us to get an idea of the amount of work required to solve
@@ -389,7 +336,8 @@ description area. Copy-paste it to retain the markdown format.
1. The change is as small as possible
1. Include proper tests and make all tests pass (unless it contains a test
- exposing a bug in existing code)
+ exposing a bug in existing code). Every new class should have corresponding
+ unit tests, even if the class is exercised at a higher level, such as a feature test.
1. If you suspect a failing CI build is unrelated to your contribution, you may
try and restart the failing CI job or ask a developer to fix the
aforementioned failing test
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 619b5376684..18091983f59 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.3.3
+3.4.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index e7c7d3cc3c8..b4d6d12101f 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.7.8
+0.7.11
diff --git a/Gemfile b/Gemfile
index 68547b6fac8..194379dd687 100644
--- a/Gemfile
+++ b/Gemfile
@@ -349,5 +349,5 @@ gem 'paranoia', '~> 2.0'
gem 'health_check', '~> 2.1.0'
# System information
-gem 'vmstat', '~> 2.1.1'
+gem 'vmstat', '~> 2.2'
gem 'sys-filesystem', '~> 1.1.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 5511d718938..0c28975060c 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -772,7 +772,7 @@ GEM
coercible (~> 1.0)
descendants_tracker (~> 0.0, >= 0.0.3)
equalizer (~> 0.0, >= 0.0.9)
- vmstat (2.1.1)
+ vmstat (2.2.0)
warden (1.2.6)
rack (>= 1.0)
web-console (2.3.0)
@@ -980,7 +980,7 @@ DEPENDENCIES
unicorn-worker-killer (~> 0.4.2)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
- vmstat (~> 2.1.1)
+ vmstat (~> 2.2)
web-console (~> 2.0)
webmock (~> 1.21.0)
wikicloth (= 0.8.1)
diff --git a/README.md b/README.md
index fee93d5f9c3..3df8bfa04c7 100644
--- a/README.md
+++ b/README.md
@@ -1,6 +1,7 @@
# GitLab
[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
## Canonical source
@@ -69,7 +70,7 @@ Instructions on how to start GitLab and how to run the tests can be found in the
GitLab is a Ruby on Rails application that runs on the following software:
- Ubuntu/Debian/CentOS/RHEL
-- Ruby (MRI) 2.1
+- Ruby (MRI) 2.3
- Git 2.7.4+
- Redis 2.8+
- MySQL or PostgreSQL
diff --git a/VERSION b/VERSION
index 542e7824102..8a5bbc87ef9 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.11.0-pre
+8.12.0-pre
diff --git a/app/assets/images/koding-logo.svg b/app/assets/images/koding-logo.svg
new file mode 100644
index 00000000000..ad89d684d94
--- /dev/null
+++ b/app/assets/images/koding-logo.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14">
+ <g fill="#d6d7d9">
+ <path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/>
+ <ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/>
+ <ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
index 748084b0307..2fe46b9fd06 100644
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -1,4 +1,3 @@
-window.gl = window.gl || {};
((global) => {
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
@@ -36,4 +35,4 @@ window.gl = window.gl || {};
}
global.AbuseReports = AbuseReports;
-})(window.gl);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index 1ab3c2197d8..5ea6086ab77 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -26,7 +26,7 @@
event_filters = $.cookie("event_filter");
filter = sender.attr("id").split("_")[0];
$.cookie("event_filter", (event_filters !== filter ? filter : ""), {
- path: '/'
+ path: gon.relative_url_root || '/'
});
if (event_filters !== filter) {
return sender.closest('li').toggleClass("active");
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index a122fa2d637..43a679501a7 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -26,8 +26,6 @@
/*= require bootstrap/tooltip */
/*= require bootstrap/popover */
/*= require select2 */
-/*= require ace-rails-ap */
-/*= require ace/ext-searchbox */
/*= require underscore */
/*= require dropzone */
/*= require mousetrap */
@@ -153,7 +151,9 @@
});
});
$('.remove-row').bind('ajax:success', function() {
- return $(this).closest('li').fadeOut();
+ $(this).tooltip('destroy')
+ .closest('li')
+ .fadeOut();
});
$('.js-remove-tr').bind('ajax:before', function() {
return $(this).hide();
@@ -288,7 +288,7 @@
new Aside();
if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') {
$.cookie('pin_nav', 'false', {
- path: '/',
+ path: gon.relative_url_root || '/',
expires: 365 * 10
});
$('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned');
@@ -313,7 +313,7 @@
$topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded');
}
$.cookie('pin_nav', doPinNav, {
- path: '/',
+ path: gon.relative_url_root || '/',
expires: 365 * 10
});
if ($.cookie('pin_nav') === 'true' || doPinNav) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index aee1c29eee3..ad12cb906e1 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -320,6 +320,7 @@
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
frequentlyUsedEmojis.push(emoji);
return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), {
+ path: gon.relative_url_root || '/',
expires: 365
});
};
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 1b7b63489ea..8ac1ba7665e 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,10 +1,26 @@
-(function() {
+(function(w) {
$(function() {
- return $("body").on("click", ".js-toggle-button", function(e) {
- $(this).find('i').toggleClass('fa fa-chevron-down').toggleClass('fa fa-chevron-up');
- $(this).closest(".js-toggle-container").find(".js-toggle-content").toggle();
- return e.preventDefault();
+ $('.js-toggle-button').on('click', function(e) {
+ e.preventDefault();
+ $(this)
+ .find('.fa')
+ .toggleClass('fa-chevron-down fa-chevron-up')
+ .end()
+ .closest('.js-toggle-container')
+ .find('.js-toggle-content')
+ .toggle()
+ ;
});
- });
-}).call(this);
+ // If we're accessing a permalink, ensure it is not inside a
+ // closed js-toggle-container!
+ var hash = w.gl.utils.getLocationHash();
+ var anchor = hash && document.getElementById(hash);
+ var container = anchor && $(anchor).closest('.js-toggle-container');
+
+ if (container && container.find('.js-toggle-content').is(':hidden')) {
+ container.find('.js-toggle-button').trigger('click');
+ anchor.scrollIntoView();
+ }
+ });
+})(window);
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
new file mode 100644
index 00000000000..2afef43f3d6
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -0,0 +1,12 @@
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var url = $(".js-edit-blob-form").data("relative-url-root");
+ url += $(".js-edit-blob-form").data("assets-prefix");
+
+ var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
+ new NewCommitForm($('.js-edit-blob-form'));
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 649c79daee8..649c79daee8 100644
--- a/app/assets/javascripts/blob/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 2c65d4427be..91c12570e09 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -38,7 +38,7 @@ $(() => {
ready () {
Store.disabled = this.disabled;
gl.boardService.all()
- .then((resp) => {
+ .then((resp) => {
resp.json().forEach((board) => {
const list = Store.addList(board);
@@ -54,4 +54,11 @@ $(() => {
});
}
});
+
+ gl.IssueBoardsSearch = new Vue({
+ el: '#js-boards-seach',
+ data: {
+ filters: Store.state.filters
+ }
+ });
});
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index e17784e7948..7e86f001f44 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -21,15 +21,10 @@
},
data () {
return {
- query: '',
filters: Store.state.filters
};
},
watch: {
- query () {
- this.list.filters = this.getFilterData();
- this.list.getIssues(true);
- },
filters: {
handler () {
this.list.page = 1;
@@ -38,16 +33,6 @@
deep: true
}
},
- methods: {
- getFilterData () {
- const filters = this.filters;
- let queryData = { search: this.query };
-
- Object.keys(filters).forEach((key) => { queryData[key] = filters[key]; });
-
- return queryData;
- }
- },
ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
@@ -55,7 +40,7 @@
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: (e) => {
- document.body.classList.remove('is-dragging');
+ gl.issueBoards.onEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(),
@@ -72,10 +57,6 @@
}
});
- if (bp.getBreakpointSize() === 'xs') {
- options.handle = '.js-board-drag-handle';
- }
-
this.sortable = Sortable.create(this.$el.parentNode, options);
},
beforeDestroy () {
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 1503d14c508..a6644e9eb8c 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -63,6 +63,8 @@
Store.moving.issue = card.issue;
Store.moving.list = card.list;
+
+ gl.issueBoards.onStart();
},
onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
@@ -72,10 +74,6 @@
}
});
- if (bp.getBreakpointSize() === 'xs') {
- options.handle = '.js-card-drag-handle';
- }
-
this.sortable = Sortable.create(this.$els.list, options);
// Scroll event on list to load more
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index b7afe4897b6..44addb3ea98 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -2,6 +2,19 @@
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
+ gl.issueBoards.onStart = () => {
+ $('.has-tooltip').tooltip('hide')
+ .tooltip('disable');
+ document.body.classList.add('is-dragging');
+ };
+
+ gl.issueBoards.onEnd = () => {
+ $('.has-tooltip').tooltip('enable');
+ document.body.classList.remove('is-dragging');
+ };
+
+ gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
let defaultSortOptions = {
forceFallback: true,
@@ -9,14 +22,11 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.has-tooltip',
- scrollSensitivity: 100,
+ delay: gl.issueBoards.touchEnabled ? 100 : 0,
+ scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
- onStart () {
- document.body.classList.add('is-dragging');
- },
- onEnd () {
- document.body.classList.remove('is-dragging');
- }
+ onStart: gl.issueBoards.onStart,
+ onEnd: gl.issueBoards.onEnd
}
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index e81e91fe972..583829552cd 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -3,6 +3,7 @@ class ListLabel {
this.id = obj.id;
this.title = obj.title;
this.color = obj.color;
+ this.textColor = obj.text_color;
this.description = obj.description;
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
}
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index be2b8c568a8..816fa49516c 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -58,10 +58,6 @@ class List {
}
}
- canSearch () {
- return this.type === 'backlog';
- }
-
getIssues (emptyIssues = true) {
const filters = this.filters;
let data = { page: this.page };
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index 18f26a1f911..bd07ee0c161 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -15,7 +15,8 @@
author_id: gl.utils.getParameterValues('author_id')[0],
assignee_id: gl.utils.getParameterValues('assignee_id')[0],
milestone_title: gl.utils.getParameterValues('milestone_title')[0],
- label_name: gl.utils.getParameterValues('label_name[]')
+ label_name: gl.utils.getParameterValues('label_name[]'),
+ search: ''
};
},
addList (listObj) {
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
index 75f8b730195..75f8b730195 100755..100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 74c4ab563f9..ba64d2bcf0b 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -20,6 +20,9 @@
path = page.split(':');
shortcut_handler = null;
switch (page) {
+ case 'projects:boards:show':
+ shortcut_handler = new ShortcutsNavigation();
+ break;
case 'projects:issues:index':
Issuable.init();
new IssuableBulkActions();
@@ -126,10 +129,12 @@
new NotificationsDropdown();
break;
case 'groups:group_members:index':
+ new gl.MemberExpirationDate();
new GroupMembers();
new UsersSelect();
break;
case 'projects:project_members:index':
+ new gl.MemberExpirationDate();
new ProjectMembers();
new UsersSelect();
break;
@@ -171,6 +176,7 @@
new BuildArtifacts();
break;
case 'projects:group_links:index':
+ new gl.MemberExpirationDate();
new GroupsSelect();
break;
case 'search:show':
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index b2e49b71fec..3fb3b1a8b51 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -39,12 +39,13 @@
FilesCommentButton.prototype.render = function(e) {
var $currentTarget, buttonParentElement, lineContentElement, textFileElement;
$currentTarget = $(e.currentTarget);
+
buttonParentElement = this.getButtonParent($currentTarget);
- if (!this.shouldRender(e, buttonParentElement)) {
- return;
- }
- textFileElement = this.getTextFileElement($currentTarget);
+ if (!this.validateButtonParent(buttonParentElement)) return;
lineContentElement = this.getLineContent($currentTarget);
+ if (!this.validateLineContent(lineContentElement)) return;
+
+ textFileElement = this.getTextFileElement($currentTarget);
buttonParentElement.append(this.buildButton({
noteableType: textFileElement.attr('data-noteable-type'),
noteableID: textFileElement.attr('data-noteable-id'),
@@ -119,10 +120,14 @@
return newButtonParent.is(this.getButtonParent($(e.currentTarget)));
};
- FilesCommentButton.prototype.shouldRender = function(e, buttonParentElement) {
+ FilesCommentButton.prototype.validateButtonParent = function(buttonParentElement) {
return !buttonParentElement.hasClass(EMPTY_CELL_CLASS) && !buttonParentElement.hasClass(UNFOLDABLE_LINE_CLASS) && $(COMMENT_BUTTON_CLASS, buttonParentElement).length === 0;
};
+ FilesCommentButton.prototype.validateLineContent = function(lineContentElement) {
+ return lineContentElement.attr('data-discussion-id') && lineContentElement.attr('data-discussion-id') !== '';
+ };
+
return FilesCommentButton;
})();
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d3394fae3f9..5a2a8523d9f 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -31,8 +31,7 @@
this.input
.on('keydown', function (e) {
var keyCode = e.which;
-
- if (keyCode === 13) {
+ if (keyCode === 13 && !options.elIsInput) {
e.preventDefault()
}
})
@@ -47,7 +46,7 @@
} else if (this.input.val() === "" && $inputContainer.hasClass(HAS_VALUE_CLASS)) {
$inputContainer.removeClass(HAS_VALUE_CLASS);
}
- if (keyCode === 13) {
+ if (keyCode === 13 && !options.elIsInput) {
return false;
}
if (this.options.remote) {
@@ -111,14 +110,14 @@
matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
if (!$el.is('.dropdown-header')) {
if (matches.length) {
- return $el.show();
+ return $el.show().removeClass('option-hidden');
} else {
- return $el.hide();
+ return $el.hide().addClass('option-hidden');
}
}
});
} else {
- return elements.show();
+ return elements.show().removeClass('option-hidden');
}
}
};
@@ -179,7 +178,7 @@
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
LOADING_CLASS = "is-loading";
@@ -191,6 +190,12 @@
currentIndex = -1;
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+
+ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ", .option-hidden)";
+
+ CURSOR_SELECT_SCROLL_PADDING = 5
+
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
function GitLabDropdown(el1, options) {
@@ -213,6 +218,7 @@
if (this.options.data) {
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.fullData = this.options.data;
+ currentIndex = -1;
this.parseData(this.options.data);
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
@@ -232,6 +238,7 @@
}
if (this.options.filterable) {
this.filter = new GitLabDropdownFilter(this.filterInput, {
+ elIsInput: $(this.el).is('input'),
filterInputBlur: this.filterInputBlur,
filterByText: this.options.filterByText,
onFilter: this.options.onFilter,
@@ -240,7 +247,7 @@
keys: searchFields,
elements: (function(_this) {
return function() {
- selector = '.dropdown-content li:not(.divider)';
+ selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
@@ -256,12 +263,16 @@
return function(data) {
_this.parseData(data);
if (_this.filterInput.val() !== '') {
- selector = '.dropdown-content li:not(.divider):visible';
+ selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
- $(selector, _this.dropdown).first().find('a').addClass('is-focused');
- return currentIndex = 0;
+ if ($(_this.el).is('input')) {
+ currentIndex = -1;
+ } else {
+ $(selector, _this.dropdown).first().find('a').addClass('is-focused');
+ currentIndex = 0;
+ }
}
};
})(this)
@@ -376,7 +387,7 @@
var $target;
if (this.options.multiSelect) {
$target = $(e.target);
- if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
+ if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
e.stopPropagation();
return false;
} else {
@@ -387,7 +398,7 @@
GitLabDropdown.prototype.opened = function() {
var contentHtml;
- currentIndex = -1;
+ this.resetRows();
this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
@@ -410,6 +421,7 @@
GitLabDropdown.prototype.hidden = function(e) {
var $input;
+ this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
@@ -463,14 +475,15 @@
return "<li class='separator'></li>";
}
if (data.header != null) {
- return "<li class='dropdown-header'>" + data.header + "</li>";
+ return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
}
if (this.options.renderRow) {
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
- fieldName = this.options.fieldName;
+ fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName;
+
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
selected = true;
@@ -494,11 +507,16 @@
text = this.highlightTextMatches(text, this.filterInput.val());
}
if (group) {
- groupAttrs = "data-group='" + group + "' data-index='" + index + "'";
+ groupAttrs = 'data-group=' + group + ' data-index=' + index;
} else {
groupAttrs = '';
}
- html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>";
+ html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
+ url: url,
+ groupAttrs: groupAttrs,
+ cssClass: cssClass,
+ text: text
+ });
}
return html;
};
@@ -520,20 +538,8 @@
return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
};
- GitLabDropdown.prototype.highlightRow = function(index) {
- var selector;
- if (this.filterInput.val() !== "") {
- selector = '.dropdown-content li:first-child a';
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content li:first-child a";
- }
- return this.getElement(selector).addClass('is-focused');
- }
- };
-
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
- fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
@@ -545,6 +551,7 @@
selectedObject = this.renderedData[selectedIndex];
}
}
+ fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
@@ -558,11 +565,6 @@
} else {
field.remove();
}
- if (this.options.toggleLabel) {
- return this.updateLabel(selectedObject, el, this);
- } else {
- return selectedObject;
- }
} else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
@@ -570,9 +572,8 @@
field.remove();
}
if (!field.length && fieldName) {
- this.addInput(fieldName, value);
+ this.addInput(fieldName, value, selectedObject);
}
- return selectedObject;
} else {
if (!this.options.multiSelect || el.hasClass('dropdown-clear-active')) {
this.dropdown.find("." + ACTIVE_CLASS).removeClass(ACTIVE_CLASS);
@@ -584,38 +585,54 @@
field.remove();
}
el.addClass(ACTIVE_CLASS);
- if (this.options.toggleLabel) {
- this.updateLabel(selectedObject, el, this);
- }
if (value != null) {
if (!field.length && fieldName) {
- this.addInput(fieldName, value);
+ this.addInput(fieldName, value, selectedObject);
} else {
field.val(value).trigger('change');
}
}
- return selectedObject;
}
+
+ // Update label right after input has been added
+ if (this.options.toggleLabel) {
+ this.updateLabel(selectedObject, el, this);
+ }
+
+ return selectedObject;
};
- GitLabDropdown.prototype.addInput = function(fieldName, value) {
+ GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
+ if (selectedObject && selectedObject.type) {
+ $input.attr('data-type', selectedObject.type);
+ }
return this.dropdown.before($input);
};
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
- selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a";
+ // If we pass an option index
+ if (typeof index !== "undefined") {
+ selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
+ } else {
+ selector = ".dropdown-content .is-focused";
+ }
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
$el = $(selector, this.dropdown);
if ($el.length) {
- return $el.first().trigger('click');
+ var href = $el.attr('href');
+ if (href && href !== '#') {
+ Turbolinks.visit(href);
+ } else {
+ $el.first().trigger('click');
+ }
}
};
@@ -623,7 +640,7 @@
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field");
- selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible';
+ selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
@@ -651,7 +668,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
- return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1);
+ _this.selectRowAtIndex();
}
};
})(this));
@@ -661,6 +678,11 @@
return $('body').off('keydown');
};
+ GitLabDropdown.prototype.resetRows = function resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ };
+
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
$('.is-focused', this.dropdown).removeClass('is-focused');
@@ -674,10 +696,14 @@
listItemHeight = $listItem.outerHeight();
listItemTop = $listItem.prop('offsetTop');
listItemBottom = listItemTop + listItemHeight;
- if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
- return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom);
- } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
- return $dropdownContent.scrollTop(listItemTop - dropdownContentTop);
+ if (!index) {
+ $dropdownContent.scrollTop(0)
+ } else if (index === ($listItems.length - 1)) {
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+ } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
}
};
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 297d4f029f0..b7f92ae9883 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -102,20 +102,34 @@
};
IssuableForm.prototype.initMoveDropdown = function() {
- var $moveDropdown;
+ var $moveDropdown, pageSize;
$moveDropdown = $('.js-move-dropdown');
if ($moveDropdown.length) {
+ pageSize = $moveDropdown.data('page-size');
return $('.js-move-dropdown').select2({
ajax: {
url: $moveDropdown.data('projects-url'),
- results: function(data) {
+ quietMillis: 125,
+ data: function(term, page, context) {
return {
- results: data
+ search: term,
+ offset_id: context
};
},
- data: function(query) {
+ results: function(data) {
+ var context,
+ more;
+
+ if (data.length >= pageSize)
+ more = true;
+
+ if (data[data.length - 1])
+ context = data[data.length - 1].id;
+
return {
- search: query
+ results: data,
+ more: more,
+ context: context
};
}
},
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 0526430989f..565dbeacdb3 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,7 +4,7 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
labelUrl = $dropdown.data('labels');
@@ -21,6 +21,7 @@
$block = $selectbox.closest('.block');
$form = $dropdown.closest('form');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL != null) {
@@ -31,7 +32,11 @@
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
- new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+ $sidebarLabelTooltip.tooltip();
+
+ if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
+ new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+ }
saveLabelData = function() {
var data, selected;
@@ -52,7 +57,7 @@
dataType: 'JSON',
data: data
}).done(function(data) {
- var labelCount, template;
+ var labelCount, template, labelTooltipTitle, labelTitles;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
@@ -66,6 +71,27 @@
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
+
+ if (data.labels.length) {
+ labelTitles = data.labels.map(function(label) {
+ return label.title;
+ });
+
+ if (labelTitles.length > 5) {
+ labelTitles = labelTitles.slice(0, 5);
+ labelTitles.push('and ' + (data.labels.length - 5) + ' more');
+ }
+
+ labelTooltipTitle = labelTitles.join(', ');
+ } else {
+ labelTooltipTitle = '';
+ $sidebarLabelTooltip.tooltip('destroy');
+ }
+
+ $sidebarLabelTooltip
+ .attr('title', labelTooltipTitle)
+ .tooltip('fixTitle');
+
$('.has-tooltip', $value).tooltip({
container: 'body'
});
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
new file mode 100644
index 00000000000..4cdf99cae72
--- /dev/null
+++ b/app/assets/javascripts/lib/ace.js
@@ -0,0 +1,2 @@
+/*= require ace-rails-ap */
+/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 10afa7e4329..d4d5927d3b0 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -67,6 +67,14 @@
$.timeago.settings.strings = tmpLocale;
};
+ w.gl.utils.getDayDifference = function(a, b) {
+ var millisecondsPerDay = 1000 * 60 * 60 * 24;
+ var date1 = Date.UTC(a.getFullYear(), a.getMonth(), a.getDate());
+ var date2 = Date.UTC(b.getFullYear(), b.getMonth(), b.getDate());
+
+ return Math.floor((date2 - date1) / millisecondsPerDay);
+ }
+
})(window);
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index fffbfd19745..533310cc87c 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -43,7 +43,7 @@
}
return newUrl;
};
- return w.gl.utils.removeParamQueryString = function(url, param) {
+ w.gl.utils.removeParamQueryString = function(url, param) {
var urlVariables, variables;
url = decodeURIComponent(url);
urlVariables = url.split('&');
@@ -59,6 +59,16 @@
return results;
})()).join('&');
};
+ w.gl.utils.getLocationHash = function(url) {
+ var hashIndex;
+ if (typeof url === 'undefined') {
+ // Note: We can't use window.location.hash here because it's
+ // not consistent across browsers - Firefox will pre-decode it
+ url = window.location.href;
+ }
+ hashIndex = url.indexOf('#');
+ return hashIndex === -1 ? null : url.substring(hashIndex + 1);
+ };
})(window);
}).call(this);
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 218f24fe908..e5d4fd44c96 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,50 +1,12 @@
(function() {
- var clearHighlights, currentTimer, defaultClass, delay, firstPiece, pieceIndex, pieces, start, stop, work;
-
Turbolinks.enableProgressBar();
- defaultClass = 'tanuki-shape';
-
- pieces = ['path#tanuki-right-cheek', 'path#tanuki-right-eye, path#tanuki-right-ear', 'path#tanuki-nose', 'path#tanuki-left-eye, path#tanuki-left-ear', 'path#tanuki-left-cheek'];
-
- pieceIndex = 0;
-
- firstPiece = pieces[0];
-
- currentTimer = null;
-
- delay = 150;
-
- clearHighlights = function() {
- return $("." + defaultClass + ".highlight").attr('class', defaultClass);
- };
-
start = function() {
- clearHighlights();
- pieceIndex = 0;
- if (pieces[0] !== firstPiece) {
- pieces.reverse();
- }
- if (currentTimer) {
- clearInterval(currentTimer);
- }
- return currentTimer = setInterval(work, delay);
+ $('.tanuki-logo').addClass('animate');
};
stop = function() {
- clearInterval(currentTimer);
- return clearHighlights();
- };
-
- work = function() {
- clearHighlights();
- $(pieces[pieceIndex]).attr('class', defaultClass + " highlight");
- if (pieceIndex === pieces.length - 1) {
- pieceIndex = 0;
- return pieces.reverse();
- } else {
- return pieceIndex++;
- }
+ $('.tanuki-logo').removeClass('animate');
};
$(document).on('page:fetch', start);
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
new file mode 100644
index 00000000000..1935af491f7
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -0,0 +1,32 @@
+(function() {
+ // Add datepickers to all `js-access-expiration-date` elements. If those elements are
+ // children of an element with the `clearable-input` class, and have a sibling
+ // `js-clear-input` element, then show that element when there is a value in the
+ // datepicker, and make clicking on that element clear the field.
+ //
+ gl.MemberExpirationDate = function() {
+ function toggleClearInput() {
+ $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+ }
+
+ var inputs = $('.js-access-expiration-date');
+
+ inputs.datepicker({
+ dateFormat: 'yy-mm-dd',
+ minDate: 1,
+ onSelect: toggleClearInput
+ });
+
+ inputs.next('.js-clear-input').on('click', function(event) {
+ event.preventDefault();
+
+ var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+ input.datepicker('setDate', null);
+ toggleClearInput.call(input);
+ });
+
+ inputs.on('blur', toggleClearInput);
+
+ inputs.each(toggleClearInput);
+ };
+}).call(this);
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
index 77bffbcb403..b56fd5aa658 100644
--- a/app/assets/javascripts/merge_conflict_resolver.js.es6
+++ b/app/assets/javascripts/merge_conflict_resolver.js.es6
@@ -75,10 +75,8 @@ class MergeConflictResolver {
window.location.href = data.redirect_to;
})
.error(() => {
- new Flash('Something went wrong!');
- })
- .always(() => {
this.vue.isSubmitting = false;
+ new Flash('Something went wrong!');
});
}
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index b97f6d22715..66e097c0a28 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -17,19 +17,15 @@
return $(this).parents('form').submit();
});
$('.hide-no-ssh-message').on('click', function(e) {
- var path;
- path = '/';
$.cookie('hide_no_ssh_message', 'false', {
- path: path
+ path: gon.relative_url_root || '/'
});
$(this).parents('.no-ssh-key-message').remove();
return e.preventDefault();
});
$('.hide-no-password-message').on('click', function(e) {
- var path;
- path = '/';
$.cookie('hide_no_password_message', 'false', {
- path: path
+ path: gon.relative_url_root || '/'
});
$(this).parents('.no-password-message').remove();
return e.preventDefault();
@@ -65,7 +61,8 @@
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref')
- }
+ },
+ dataType: "json"
}).done(function(refs) {
return callback(refs);
});
@@ -73,7 +70,7 @@
selectable: true,
filterable: true,
filterByText: true,
- fieldName: 'ref',
+ fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
var link;
if (ref.header != null) {
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
index f6a796b325a..78f7b48bc7d 100644
--- a/app/assets/javascripts/project_members.js
+++ b/app/assets/javascripts/project_members.js
@@ -5,9 +5,6 @@
return $(this).fadeOut();
});
}
-
return ProjectMembers;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6
index 2fbb088fa04..7aeb5f92514 100644
--- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6
@@ -10,8 +10,12 @@
selectable: true,
inputId: $dropdown.data('input-id'),
fieldName: $dropdown.data('field-name'),
- toggleLabel(item) {
- return item.text;
+ toggleLabel(item, el) {
+ if (el.is('.is-active')) {
+ return item.text;
+ } else {
+ return 'Select';
+ }
},
clicked(item, $el, e) {
e.preventDefault();
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6
index 2efca2414dc..46beca469b9 100644
--- a/app/assets/javascripts/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branch_create.js.es6
@@ -47,9 +47,7 @@
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
- if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
- this.$form.find('input[type="submit"]').removeAttr('disabled');
- }
+ this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
}
}
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
index a59fcbfa082..40bc4adb71b 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -31,6 +31,9 @@
const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
+ // Do not update if one dropdown has not selected any option
+ if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index dc4d5113826..e3d5f413c77 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -30,7 +30,7 @@
}
if (!triggered) {
return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), {
- path: '/'
+ path: gon.relative_url_root || '/'
});
}
});
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 990f6536eb2..227e8c696b4 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -7,7 +7,9 @@
KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
- ENTER: 13
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
};
function SearchAutocomplete(opts) {
@@ -223,6 +225,12 @@
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
+ case KEYCODE.ENTER:
+ this.disableAutocomplete();
+ break;
+ case KEYCODE.UP:
+ case KEYCODE.DOWN:
+ return;
default:
if (this.searchInput.val() === '') {
this.disableAutocomplete();
@@ -319,9 +327,11 @@
};
SearchAutocomplete.prototype.disableAutocomplete = function() {
- this.searchInput.addClass('disabled');
- this.dropdown.removeClass('open');
- return this.restoreMenu();
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ this.searchInput.addClass('disabled');
+ this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.restoreMenu();
+ }
};
SearchAutocomplete.prototype.restoreMenu = function() {
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
new file mode 100644
index 00000000000..855e97eb301
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -0,0 +1,12 @@
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var editor = ace.edit("editor")
+
+ $(".snippet-form-holder form").on('submit', function() {
+ $(".snippet-file-content").val(editor.getValue());
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/user.js b/app/assets/javascripts/user.js
index b46390ad8f4..6c4d88cf407 100644
--- a/app/assets/javascripts/user.js
+++ b/app/assets/javascripts/user.js
@@ -7,10 +7,8 @@
});
this.initTabs();
$('.hide-project-limit-message').on('click', function(e) {
- var path;
- path = '/';
$.cookie('hide_project_limit_message', 'false', {
- path: path
+ path: gon.relative_url_root || '/'
});
$(this).parents('.project-limit-message').remove();
return e.preventDefault();
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 8b3dbf5f5ae..74ecf4f4cf9 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -3,7 +3,6 @@
this.Calendar = (function() {
function Calendar(timestamps, calendar_activities_path) {
- var group, i;
this.calendar_activities_path = calendar_activities_path;
this.clickDay = bind(this.clickDay, this);
this.currentSelectedDate = '';
@@ -13,26 +12,36 @@
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.months = [];
this.timestampsTmp = [];
- i = 0;
- group = 0;
- _.each(timestamps, (function(_this) {
- return function(count, date) {
- var day, innerArray, newDate;
- newDate = new Date(parseInt(date) * 1000);
- day = newDate.getDay();
- if ((day === 0 && i !== 0) || i === 0) {
- _this.timestampsTmp.push([]);
- group++;
- }
- innerArray = _this.timestampsTmp[group - 1];
- innerArray.push({
- count: count,
- date: newDate,
- day: day
- });
- return i++;
- };
- })(this));
+ var group = 0;
+
+ var today = new Date()
+ today.setHours(0, 0, 0, 0, 0);
+
+ var oneYearAgo = new Date(today);
+ oneYearAgo.setFullYear(today.getFullYear() - 1);
+
+ var days = gl.utils.getDayDifference(oneYearAgo, today);
+
+ for(var i = 0; i <= days; i++) {
+ var date = new Date(oneYearAgo);
+ date.setDate(date.getDate() + i);
+
+ var day = date.getDay();
+ var count = timestamps[date.getTime() * 0.001];
+
+ if ((day === 0 && i !== 0) || i === 0) {
+ this.timestampsTmp.push([]);
+ group++;
+ }
+
+ var innerArray = this.timestampsTmp[group - 1];
+ innerArray.push({
+ count: count || 0,
+ date: date,
+ day: day
+ });
+ }
+
this.colorKey = this.initColorKey();
this.color = this.initColor();
this.renderSvg(group);
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index a306b8f3f29..d5cca1b10fb 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -24,6 +24,7 @@
@import "framework/issue_box.scss";
@import "framework/jquery.scss";
@import "framework/lists.scss";
+@import "framework/logo.scss";
@import "framework/markdown_area.scss";
@import "framework/mobile.scss";
@import "framework/modal.scss";
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 6c3786b49bb..cd3ddf5fee9 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -200,7 +200,7 @@
svg {
height: 15px;
- width: auto;
+ width: 15px;
position: relative;
top: 2px;
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index c1e5305644b..8984bce616c 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -248,7 +248,7 @@ li.note {
img.emoji {
height: 20px;
- vertical-align: middle;
+ vertical-align: top;
width: 20px;
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index f1635a53763..edb2ff01f88 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -17,6 +17,12 @@
.dropdown {
position: relative;
+
+ .btn-link {
+ &:hover {
+ cursor: pointer;
+ }
+ }
}
.open {
@@ -84,6 +90,15 @@
width: 100%;
}
}
+
+ // Allows dynamic-width text in the dropdown toggle.
+ // Resizes to allow long text without overflowing the container.
+ &.dynamic {
+ width: auto;
+ min-width: 160px;
+ max-width: 100%;
+ padding-right: 25px;
+ }
}
.dropdown-menu,
@@ -180,6 +195,12 @@
.separator + .dropdown-header {
padding-top: 2px;
}
+
+ .unclickable {
+ cursor: not-allowed;
+ padding: 5px 8px;
+ color: $dropdown-header-color;
+ }
}
.dropdown-menu-large {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 407f1873431..d3e3fc50736 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -63,9 +63,10 @@
&.image_file {
background: #eee;
text-align: center;
+
img {
- padding: 100px;
- max-width: 50%;
+ padding: 20px;
+ max-width: 80%;
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 43d55661541..37ff7e22ed1 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -19,7 +19,6 @@ input[type='text'].danger {
}
.form-actions {
- margin: -$gl-padding;
margin-top: 0;
margin-bottom: -$gl-padding;
padding: $gl-padding;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 0c607071840..afe4a276ae5 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -2,16 +2,6 @@
* Application Header
*
*/
-@mixin tanuki-logo-colors($path-color) {
- fill: $path-color;
- transition: all 0.8s;
-
- &:hover,
- &.highlight {
- fill: lighten($path-color, 25%);
- transition: all 0.1s;
- }
-}
header {
transition: padding $sidebar-transition-duration;
@@ -25,7 +15,7 @@ header {
margin: 8px 0;
text-align: center;
- #tanuki-logo, img {
+ .tanuki-logo, img {
height: 36px;
}
}
@@ -205,26 +195,6 @@ header {
}
}
-#tanuki-logo {
-
- #tanuki-left-ear,
- #tanuki-right-ear,
- #tanuki-nose {
- @include tanuki-logo-colors($tanuki-red);
- }
-
- #tanuki-left-eye,
- #tanuki-right-eye {
- @include tanuki-logo-colors($tanuki-orange);
- }
-
- #tanuki-left-cheek,
- #tanuki-right-cheek {
- @include tanuki-logo-colors($tanuki-yellow);
- }
-
-}
-
@media (max-width: $screen-xs-max) {
header .container-fluid {
font-size: 18px;
diff --git a/app/assets/stylesheets/framework/logo.scss b/app/assets/stylesheets/framework/logo.scss
new file mode 100644
index 00000000000..3ee3fb4cee5
--- /dev/null
+++ b/app/assets/stylesheets/framework/logo.scss
@@ -0,0 +1,118 @@
+@mixin unique-keyframes {
+ $animation-name: unique-id();
+ @include webkit-prefix(animation-name, $animation-name);
+
+ @-webkit-keyframes #{$animation-name} {
+ @content;
+ }
+ @keyframes #{$animation-name} {
+ @content;
+ }
+}
+
+@mixin tanuki-logo-colors($path-color) {
+ fill: $path-color;
+ transition: all 0.8s;
+
+ &:hover {
+ fill: lighten($path-color, 25%);
+ transition: all 0.1s;
+ }
+}
+
+@mixin tanuki-second-highlight-animations($tanuki-color) {
+ @include unique-keyframes {
+ 10%, 80% {
+ fill: #{$tanuki-color}
+ }
+ 20%, 90% {
+ fill: lighten($tanuki-color, 25%);
+ }
+ }
+}
+
+@mixin tanuki-forth-highlight-animations($tanuki-color) {
+ @include unique-keyframes {
+ 30%, 60% {
+ fill: #{$tanuki-color};
+ }
+ 40%, 70% {
+ fill: lighten($tanuki-color, 25%);
+ }
+ }
+}
+
+.tanuki-logo {
+
+ .tanuki-left-ear,
+ .tanuki-right-ear,
+ .tanuki-nose {
+ @include tanuki-logo-colors($tanuki-red);
+ }
+
+ .tanuki-left-eye,
+ .tanuki-right-eye {
+ @include tanuki-logo-colors($tanuki-orange);
+ }
+
+ .tanuki-left-cheek,
+ .tanuki-right-cheek {
+ @include tanuki-logo-colors($tanuki-yellow);
+ }
+
+ &.animate {
+ .tanuki-shape {
+ @include webkit-prefix(animation-duration, 1.5s);
+ @include webkit-prefix(animation-iteration-count, infinite);
+ }
+
+ .tanuki-left-cheek {
+ @include unique-keyframes {
+ 0%, 10%, 100% {
+ fill: lighten($tanuki-yellow, 25%);
+ }
+ 90% {
+ fill: $tanuki-yellow;
+ }
+ }
+ }
+
+ .tanuki-left-eye {
+ @include tanuki-second-highlight-animations($tanuki-orange);
+ }
+
+ .tanuki-left-ear {
+ @include tanuki-second-highlight-animations($tanuki-red);
+ }
+
+ .tanuki-nose {
+ @include unique-keyframes {
+ 20%, 70% {
+ fill: $tanuki-red;
+ }
+ 30%, 80% {
+ fill: lighten($tanuki-red, 25%);
+ }
+ }
+ }
+
+ .tanuki-right-eye {
+ @include tanuki-forth-highlight-animations($tanuki-orange);
+ }
+
+ .tanuki-right-ear {
+ @include tanuki-forth-highlight-animations($tanuki-red);
+ }
+
+ .tanuki-right-cheek {
+ @include unique-keyframes {
+ 40% {
+ fill: $tanuki-yellow;
+ }
+ 60% {
+ fill: lighten($tanuki-yellow, 25%);
+ }
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index d2d60ed7196..396a37bab6e 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -9,22 +9,6 @@
border-radius: $radius;
}
-@mixin border-radius-left($radius) {
- @include border-radius($radius 0 0 $radius)
-}
-
-@mixin border-radius-right($radius) {
- @include border-radius(0 0 $radius $radius)
-}
-
-@mixin linear-gradient($from, $to) {
- background-image: -webkit-gradient(linear, 0 0, 0 100%, from($from), to($to));
- background-image: -webkit-linear-gradient($from, $to);
- background-image: -moz-linear-gradient($from, $to);
- background-image: -ms-linear-gradient($from, $to);
- background-image: -o-linear-gradient($from, $to);
-}
-
@mixin transition($transition) {
-webkit-transition: $transition;
-moz-transition: $transition;
@@ -38,14 +22,6 @@
* Mixins with fixed values
*/
-@mixin shade {
- @include box-shadow(0 0 3px #ddd);
-}
-
-@mixin solid-shade {
- @include box-shadow(0 0 0 3px #f1f1f1);
-}
-
@mixin str-truncated($max_width: 82%) {
display: inline-block;
overflow: hidden;
@@ -94,23 +70,6 @@
}
}
-@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 bulleted-list {
> ul {
list-style-type: disc;
@@ -129,3 +88,8 @@
color: rgba(255, 255, 255, 0.3);
background: rgba(255, 255, 255, 0.1);
}
+
+@mixin webkit-prefix($property, $value) {
+ #{'-webkit-' + $property}: $value;
+ #{$property}: $value;
+}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 26ad2870aa0..8374f30d0b2 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,6 +1,5 @@
.modal-body {
position: relative;
- overflow-y: auto;
padding: 15px;
.form-actions {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 9e924f99e9c..9efbaf54e90 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -1,4 +1,4 @@
-@mixin fade($gradient-direction, $rgba, $gradient-color) {
+@mixin fade($gradient-direction, $gradient-color) {
visibility: hidden;
opacity: 0;
z-index: 2;
@@ -8,10 +8,7 @@
height: 30px;
transition-duration: .3s;
-webkit-transform: translateZ(0);
- background: -webkit-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: -o-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: -moz-linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
- background: linear-gradient($gradient-direction, $rgba, $gradient-color 45%);
+ background: linear-gradient(to $gradient-direction, $gradient-color 45%, rgba($gradient-color, 0.4));
&.scrolling {
visibility: visible;
@@ -71,7 +68,7 @@
.badge {
font-weight: normal;
background-color: #eee;
- color: #78a;
+ color: $btn-transparent-color;
vertical-align: baseline;
}
}
@@ -161,6 +158,7 @@
> .dropdown {
margin-right: $gl-padding-top;
display: inline-block;
+ vertical-align: top;
&:last-child {
margin-right: 0;
@@ -210,12 +208,6 @@
}
}
- .project-filter-form {
- input {
- background-color: $background-color;
- }
- }
-
@media (max-width: $screen-xs-max) {
padding-bottom: 0;
width: 100%;
@@ -335,10 +327,6 @@
}
}
- .badge {
- color: $gl-icon-color;
- }
-
&:hover {
a, i {
color: $black;
@@ -356,7 +344,7 @@
}
.fade-right {
- @include fade(left, rgba(255, 255, 255, 0.4), $background-color);
+ @include fade(left, $background-color);
right: -5px;
.fa {
@@ -365,7 +353,7 @@
}
.fade-left {
- @include fade(right, rgba(255, 255, 255, 0.4), $background-color);
+ @include fade(right, $background-color);
left: -5px;
.fa {
@@ -376,6 +364,7 @@
&.sub-nav-scroll {
.fade-right {
+ @include fade(left, $dark-background-color);
right: 0;
.fa {
@@ -384,6 +373,7 @@
}
.fade-left {
+ @include fade(right, $dark-background-color);
left: 0;
.fa {
@@ -400,7 +390,7 @@
@include scrolling-links();
.fade-right {
- @include fade(left, rgba(255, 255, 255, 0.4), $white-light);
+ @include fade(left, $white-light);
right: -5px;
.fa {
@@ -409,7 +399,7 @@
}
.fade-left {
- @include fade(right, rgba(255, 255, 255, 0.4), $white-light);
+ @include fade(right, $white-light);
left: -5px;
.fa {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 21d87cc9d34..b2e22b60440 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -45,7 +45,8 @@
min-width: 175px;
}
-.select2-results .select2-result-label {
+.select2-results .select2-result-label,
+.select2-more-results {
padding: 10px 15px;
}
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index c9cdfdcd29c..8f71381f5c4 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -96,6 +96,10 @@
line-height: inherit;
}
}
+
+ .label-default {
+ color: $btn-transparent-color;
+ }
}
.abuse-reports {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index ad4b2d6496f..d91558bc672 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -8,9 +8,13 @@
}
.is-dragging {
+ // Important because plugin sets inline CSS
+ opacity: 1!important;
+
* {
- cursor: -webkit-grabbing;
- cursor: grabbing;
+ // !important to make sure no style can override this when dragging
+ cursor: -webkit-grabbing!important;
+ cursor: grabbing!important;
}
}
@@ -101,8 +105,8 @@
.board {
display: -webkit-flex;
display: flex;
- min-width: calc(100vw - 15px);
- max-width: calc(100vw - 15px);
+ min-width: calc(85vw - 15px);
+ max-width: calc(85vw - 15px);
margin-bottom: 25px;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
@@ -154,48 +158,6 @@
padding: $gl-padding;
font-size: 1em;
border-bottom: 1px solid $border-color;
-
- .board-mobile-handle {
- position: relative;
- left: 0;
- top: 1px;
- margin-top: 0;
- margin-right: 5px;
- }
-}
-
-.board-search-container {
- position: relative;
- background-color: #fff;
-
- .form-control {
- padding-right: 30px;
- }
-}
-
-.board-search-icon,
-.board-search-clear-btn {
- position: absolute;
- right: $gl-padding + 10px;
- top: 50%;
- margin-top: -7px;
- font-size: 14px;
-}
-
-.board-search-icon {
- color: $gl-placeholder-color;
-}
-
-.board-search-clear-btn {
- padding: 0;
- line-height: 1;
- background: transparent;
- border: 0;
- outline: 0;
-
- &:hover {
- color: $gl-link-color;
- }
}
.board-delete {
@@ -254,11 +216,6 @@
opacity: 0.3;
}
-.is-dragging {
- // Important because plugin sets inline CSS
- opacity: 1!important;
-}
-
.card {
position: relative;
width: 100%;
@@ -269,11 +226,7 @@
list-style: none;
&.user-can-drag {
- padding-left: ($gl-padding * 2);
-
- @media (min-width: $screen-sm-min) {
- padding-left: $gl-padding;
- }
+ padding-left: $gl-padding;
}
&:not(:last-child) {
@@ -294,17 +247,6 @@
}
}
-.board-mobile-handle {
- position: absolute;
- left: 10px;
- top: 50%;
- margin-top: (-15px / 2);
-
- @media (min-width: $screen-sm-min) {
- display: none;
- }
-}
-
.card-title {
margin: 0;
font-size: 1em;
@@ -316,6 +258,7 @@
.card-footer {
margin-top: 5px;
+ line-height: 25px;
.label {
margin-right: 4px;
@@ -327,3 +270,12 @@
margin-right: 8px;
font-weight: 500;
}
+
+.issue-boards-search {
+ width: 335px;
+
+ .form-control {
+ display: inline-block;
+ width: 210px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 81fce55853c..8c33e7d9a2e 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -150,7 +150,7 @@
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
max-height: 300px;
- overflow: scroll;
+ overflow: auto;
svg {
position: relative;
@@ -168,7 +168,6 @@
text-overflow: ellipsis;
&:hover {
- background-color: $row-hover;
color: $gl-text-color;
}
}
@@ -190,6 +189,10 @@
display: block;
}
}
+
+ &:hover {
+ background-color: $row-hover;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index fe8a8025038..53ec0002afe 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -68,22 +68,11 @@
}
.ci-status-link {
- margin-left: 2px;
svg {
- vertical-align: middle;
- }
-
- .ci-status-label {
- display: inline-block;
- }
-
- &:hover {
- text-decoration: none;
-
- .ci-status-label {
- text-decoration: underline;
- }
+ position: relative;
+ top: 2px;
+ margin: 0 2px 0 3px;
}
}
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 5c336bb1c7e..0cd45fb90bf 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -115,11 +115,8 @@
}
&.commits-stat {
- margin-top: 3px;
display: block;
- padding: 3px;
- padding-left: 0;
-
+ padding: 0 3px 0 0;
&:hover {
background: none;
}
diff --git a/app/assets/stylesheets/pages/import.scss b/app/assets/stylesheets/pages/import.scss
index 84cc35239f9..a4f76a9495a 100644
--- a/app/assets/stylesheets/pages/import.scss
+++ b/app/assets/stylesheets/pages/import.scss
@@ -1,22 +1,3 @@
-i.icon-gitorious {
- display: inline-block;
- background-position: 0 0;
- background-size: contain;
- background-repeat: no-repeat;
-}
-
-i.icon-gitorious-small {
- background-image: image-url('gitorious-logo-blue.png');
- width: 13px;
- height: 13px;
-}
-
-i.icon-gitorious-big {
- background-image: image-url('gitorious-logo-black.png');
- width: 18px;
- height: 18px;
-}
-
.import-jobs-from-col,
.import-jobs-to-col {
width: 40%;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index b4636269518..7fdd79fa8b9 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -269,7 +269,7 @@
.builds {
.table-holder {
- overflow-x: scroll;
+ overflow-x: auto;
}
}
@@ -374,3 +374,20 @@
}
}
}
+
+.mr-version-switch {
+ background: $background-color;
+ padding: $gl-btn-padding;
+ color: $gl-placeholder-color;
+
+ a.btn-link {
+ color: $gl-dark-link-color;
+ }
+}
+
+.merge-request-details {
+
+ .title {
+ margin-bottom: 20px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 08d1692c888..54124a3d658 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -281,19 +281,13 @@ ul.notes {
font-size: 17px;
}
- &.js-note-delete {
- i {
- &:hover {
- color: $gl-text-red;
- }
+ &:hover {
+ .danger-highlight {
+ color: $gl-text-red;
}
- }
- &.js-note-edit {
- i {
- &:hover {
- color: $gl-link-color;
- }
+ .link-highlight {
+ color: $gl-link-color;
}
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ce1c424624f..2d6653cd867 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -254,7 +254,6 @@
width: 100%;
overflow: auto;
white-space: nowrap;
- max-height: 500px;
transition: max-height 0.3s, padding 0.3s;
&.graph-collapsed {
@@ -265,7 +264,6 @@
.pipeline-visualization {
position: relative;
- min-width: 1220px;
ul {
padding: 0;
@@ -275,7 +273,7 @@
.stage-column {
display: inline-block;
vertical-align: top;
- margin-right: 50px;
+ margin-right: 65px;
li {
list-style: none;
@@ -300,6 +298,17 @@
&.playable {
background-color: $gray-light;
+
+ svg {
+ height: 12px;
+ width: 12px;
+ position: relative;
+ top: 1px;
+
+ path {
+ fill: $layout-link-gray;
+ }
+ }
}
.build-content {
@@ -310,6 +319,14 @@
a {
color: $layout-link-gray;
+ text-decoration: none;
+
+ &:hover {
+ .ci-status-text {
+ text-decoration: underline;
+ }
+ }
+
}
}
@@ -319,19 +336,15 @@
margin-right: 5px;
}
- .fa {
- font-size: 13px;
- }
-
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
content: '';
position: absolute;
top: 50%;
- right: -54px;
+ right: -69px;
border-top: 2px solid $border-color;
- width: 54px;
+ width: 69px;
height: 1px;
}
}
@@ -351,22 +364,25 @@
&::after {
right: -20px;
border-right: 2px solid $border-color;
- border-radius: 0 0 50px;
+ border-radius: 0 0 15px;
}
// Left connecting curves
&::before {
left: -20px;
border-left: 2px solid $border-color;
- border-radius: 0 0 0 50px;
+ border-radius: 0 0 0 15px;
}
}
// Connect second build to first build with smaller curved line
&:nth-child(2) {
&::after, &::before {
- height: 45px;
- top: -26px;
+ height: 29px;
+ top: -10px;
+ }
+ .curve {
+ display: block;
}
}
}
@@ -385,6 +401,12 @@
border: none;
}
}
+ // Remove opposite curve
+ .curve {
+ &::before {
+ display: none;
+ }
+ }
}
}
@@ -396,6 +418,39 @@
border: none;
}
}
+ // Remove opposite curve
+ .curve {
+ &::after {
+ display: none;
+ }
+ }
+ }
+ }
+
+ // Curve first child connecting lines in opposite direction
+ .curve {
+ display: none;
+
+ &::before,
+ &::after {
+ content: '';
+ width: 21px;
+ height: 25px;
+ position: absolute;
+ top: -28.5px;
+ border-top: 2px solid $border-color;
+ }
+
+ &::after {
+ left: -39px;
+ border-right: 2px solid $border-color;
+ border-radius: 0 15px;
+ }
+
+ &::before {
+ right: -39px;
+ border-left: 2px solid $border-color;
+ border-radius: 15px 0 0;
}
}
}
@@ -422,3 +477,10 @@
width: 60px;
}
}
+
+.ci-status-icon-created {
+
+ svg {
+ fill: $table-text-gray;
+ }
+}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 27dc2b2a1fa..f2db373da52 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -311,6 +311,14 @@ a.deploy-project-label {
color: $gl-success;
}
+.lfs-enabled {
+ color: $gl-success;
+}
+
+.lfs-disabled {
+ color: $gl-warning;
+}
+
.breadcrumb.repo-breadcrumb {
padding: 0;
background: transparent;
@@ -600,18 +608,25 @@ pre.light-well {
}
}
-.project-show-readme .readme-holder {
- padding: $gl-padding 0;
- border-top: 0;
-
- .edit-project-readme {
- z-index: 2;
- position: relative;
+.project-show-readme {
+ .row-content-block {
+ background-color: inherit;
+ border: none;
}
- .wiki h1 {
- border-bottom: none;
- padding: 0;
+ .readme-holder {
+ padding: $gl-padding 0;
+ border-top: 0;
+
+ .edit-project-readme {
+ z-index: 2;
+ position: relative;
+ }
+
+ .wiki h1 {
+ border-bottom: none;
+ padding: 0;
+ }
}
}
@@ -719,3 +734,29 @@ pre.light-well {
width: 300px;
}
}
+
+.clearable-input {
+ position: relative;
+
+ .clear-icon {
+ @extend .fa-times;
+ display: none;
+ position: absolute;
+ right: 7px;
+ top: 7px;
+ color: $location-icon-color;
+
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ }
+ }
+
+ &.has-value {
+ .clear-icon {
+ cursor: pointer;
+ display: block;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 587f2d9f3c1..0ee7ceecae5 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -43,6 +43,15 @@
border-color: $blue-normal;
}
+ &.ci-created {
+ color: $table-text-gray;
+ border-color: $table-text-gray;
+
+ svg {
+ fill: $table-text-gray;
+ }
+ }
+
svg {
height: 13px;
width: 13px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 9da40fe2b09..538f211c65b 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -11,6 +11,16 @@
}
}
+ .last-commit {
+ max-width: 506px;
+
+ .last-commit-content {
+ @include str-truncated;
+ width: calc(100% - 140px);
+ margin-left: 3px;
+ }
+ }
+
.tree-table {
margin-bottom: 0;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 9e1dc15de84..6ef7cf0bae6 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -109,6 +109,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:sentry_dsn,
:akismet_enabled,
:akismet_api_key,
+ :koding_enabled,
+ :koding_url,
:email_author_in_body,
:repository_checks_enabled,
:metrics_packet_size,
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4ce18321649..cdfa8d91a28 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -42,7 +42,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
diff --git a/app/controllers/admin/impersonations_controller.rb b/app/controllers/admin/impersonations_controller.rb
index 8be35f00a77..9433da02f64 100644
--- a/app/controllers/admin/impersonations_controller.rb
+++ b/app/controllers/admin/impersonations_controller.rb
@@ -7,7 +7,7 @@ class Admin::ImpersonationsController < Admin::ApplicationController
warden.set_user(impersonator, scope: :user)
- Gitlab::AppLogger.info("User #{original_user.username} has stopped impersonating #{impersonator.username}")
+ Gitlab::AppLogger.info("User #{impersonator.username} has stopped impersonating #{original_user.username}")
session[:impersonator_id] = nil
diff --git a/app/controllers/admin/system_info_controller.rb b/app/controllers/admin/system_info_controller.rb
index e4c73008826..ca04a17caa1 100644
--- a/app/controllers/admin/system_info_controller.rb
+++ b/app/controllers/admin/system_info_controller.rb
@@ -29,7 +29,8 @@ class Admin::SystemInfoController < Admin::ApplicationController
]
def show
- system_info = Vmstat.snapshot
+ @cpus = Vmstat.cpu rescue nil
+ @memory = Vmstat.memory rescue nil
mounts = Sys::Filesystem.mounts
@disks = []
@@ -50,10 +51,5 @@ class Admin::SystemInfoController < Admin::ApplicationController
rescue Sys::Filesystem::Error
end
end
-
- @cpus = system_info.cpus.length
-
- @mem_used = system_info.memory.active_bytes
- @mem_total = system_info.memory.total_bytes
end
end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index 634d36a4467..ebc2a4651ba 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -6,6 +6,7 @@ class ApplicationController < ActionController::Base
include Gitlab::GonHelper
include GitlabRoutingHelper
include PageLayoutHelper
+ include SentryHelper
include WorkhorseHelper
before_action :authenticate_user_from_private_token!
@@ -24,7 +25,7 @@ class ApplicationController < ActionController::Base
protect_from_forgery with: :exception
helper_method :abilities, :can?, :current_application_settings
- helper_method :import_sources_enabled?, :github_import_enabled?, :github_import_configured?, :gitlab_import_enabled?, :gitlab_import_configured?, :bitbucket_import_enabled?, :bitbucket_import_configured?, :gitorious_import_enabled?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_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?, :google_code_import_enabled?, :fogbugz_import_enabled?, :git_import_enabled?, :gitlab_project_import_enabled?
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
@@ -46,28 +47,6 @@ class ApplicationController < ActionController::Base
protected
- def sentry_context
- if Rails.env.production? && current_application_settings.sentry_enabled
- if current_user
- Raven.user_context(
- id: current_user.id,
- email: current_user.email,
- username: current_user.username,
- )
- end
-
- Raven.tags_context(program: sentry_program_context)
- end
- end
-
- def sentry_program_context
- if Sidekiq.server?
- 'sidekiq'
- else
- 'rails'
- end
- end
-
# This filter handles both private tokens and personal access tokens
def authenticate_user_from_private_token!
token_string = params[:private_token].presence || request.headers['PRIVATE-TOKEN'].presence
@@ -271,10 +250,6 @@ class ApplicationController < ActionController::Base
Gitlab::OAuth::Provider.enabled?(:bitbucket) && Gitlab::BitbucketImport.public_key.present?
end
- def gitorious_import_enabled?
- current_application_settings.import_sources.include?('gitorious')
- end
-
def google_code_import_enabled?
current_application_settings.import_sources.include?('google_code')
end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index 036777c80c1..172d5344b7a 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -8,10 +8,14 @@ module ToggleAwardEmoji
def toggle_award_emoji
name = params.require(:name)
- awardable.toggle_award_emoji(name, current_user)
- TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
+ if awardable.user_can_award?(current_user, name)
+ awardable.toggle_award_emoji(name, current_user)
+ TodoService.new.new_award_emoji(to_todoable(awardable), current_user)
- render json: { ok: true }
+ render json: { ok: true }
+ else
+ render json: { ok: false }
+ end
end
private
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index c8390af3b36..d425d0f9014 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -2,6 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy_all]
def index
+ @sort = params[:sort]
@todos = @todos.page(params[:page])
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 9fc41a12536..272164cd0cc 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ current_user: current_user,
+ expires_at: params[:expires_at]
+ )
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
@@ -63,7 +68,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
def member_params
- params.require(:group_member).permit(:access_level, :user_id)
+ params.require(:group_member).permit(:access_level, :user_id, :expires_at)
end
# MembershipActions concern
diff --git a/app/controllers/import/gitorious_controller.rb b/app/controllers/import/gitorious_controller.rb
deleted file mode 100644
index a4c4ad23027..00000000000
--- a/app/controllers/import/gitorious_controller.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-class Import::GitoriousController < Import::BaseController
- before_action :verify_gitorious_import_enabled
-
- def new
- redirect_to client.authorize_url(callback_import_gitorious_url)
- end
-
- def callback
- session[:gitorious_repos] = params[:repos]
- redirect_to status_import_gitorious_path
- end
-
- def status
- @repos = client.repos
-
- @already_added_projects = current_user.created_projects.where(import_type: "gitorious")
- already_added_projects_names = @already_added_projects.pluck(:import_source)
-
- @repos.reject! { |repo| already_added_projects_names.include? repo.full_name }
- end
-
- def jobs
- jobs = current_user.created_projects.where(import_type: "gitorious").to_json(only: [:id, :import_status])
- render json: jobs
- end
-
- def create
- @repo_id = params[:repo_id]
- repo = client.repo(@repo_id)
- @target_namespace = params[:new_namespace].presence || repo.namespace
- @project_name = repo.name
-
- namespace = get_or_create_namespace || (render and return)
-
- @project = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, current_user).execute
- end
-
- private
-
- def client
- @client ||= Gitlab::GitoriousImport::Client.new(session[:gitorious_repos])
- end
-
- def verify_gitorious_import_enabled
- render_404 unless gitorious_import_enabled?
- end
-end
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
new file mode 100644
index 00000000000..f3759b4c0ea
--- /dev/null
+++ b/app/controllers/koding_controller.rb
@@ -0,0 +1,15 @@
+class KodingController < ApplicationController
+ before_action :check_integration!, :authenticate_user!, :reject_blocked!
+ layout 'koding'
+
+ def index
+ path = File.join(Rails.root, 'doc/user/project/koding.md')
+ @markdown = File.read(path)
+ end
+
+ private
+
+ def check_integration!
+ render_404 unless current_application_settings.koding_enabled?
+ end
+end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index 7241949393b..59222637961 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -1,22 +1,25 @@
class Projects::ArtifactsController < Projects::ApplicationController
+ include ExtractsPath
+
layout 'project'
before_action :authorize_read_build!
before_action :authorize_update_build!, only: [:keep]
+ before_action :extract_ref_name_and_path
before_action :validate_artifacts!
def download
- unless artifacts_file.file_storage?
- return redirect_to artifacts_file.url
+ if artifacts_file.file_storage?
+ send_file artifacts_file.path, disposition: 'attachment'
+ else
+ redirect_to artifacts_file.url
end
-
- send_file artifacts_file.path, disposition: 'attachment'
end
def browse
directory = params[:path] ? "#{params[:path]}/" : ''
@entry = build.artifacts_metadata_entry(directory)
- return render_404 unless @entry.exists?
+ render_404 unless @entry.exists?
end
def file
@@ -34,14 +37,41 @@ class Projects::ArtifactsController < Projects::ApplicationController
redirect_to namespace_project_build_path(project.namespace, project, build)
end
+ def latest_succeeded
+ target_path = artifacts_action_path(@path, project, build)
+
+ if target_path
+ redirect_to(target_path)
+ else
+ render_404
+ end
+ end
+
private
+ def extract_ref_name_and_path
+ return unless params[:ref_name_and_path]
+
+ @ref_name, @path = extract_ref(params[:ref_name_and_path])
+ end
+
def validate_artifacts!
- render_404 unless build.artifacts?
+ render_404 unless build && build.artifacts?
end
def build
- @build ||= project.builds.find_by!(id: params[:build_id])
+ @build ||= build_from_id || build_from_ref
+ end
+
+ def build_from_id
+ project.builds.find_by(id: params[:build_id]) if params[:build_id]
+ end
+
+ def build_from_ref
+ return unless @ref_name
+
+ builds = project.latest_successful_builds_for(@ref_name)
+ builds.find_by(name: params[:job])
end
def artifacts_file
diff --git a/app/controllers/projects/avatars_controller.rb b/app/controllers/projects/avatars_controller.rb
index 5962f74c39b..ada7db3c552 100644
--- a/app/controllers/projects/avatars_controller.rb
+++ b/app/controllers/projects/avatars_controller.rb
@@ -4,7 +4,7 @@ class Projects::AvatarsController < Projects::ApplicationController
before_action :authorize_admin_project!, only: [:destroy]
def show
- @blob = @repository.blob_at_branch('master', @project.avatar_in_git)
+ @blob = @repository.blob_at_branch(@repository.root_ref, @project.avatar_in_git)
if @blob
headers['X-Content-Type-Options'] = 'nosniff'
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 2d894b3dd4a..1a4f6b50e8f 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -12,7 +12,7 @@ module Projects
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority] }
+ labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 48fe81b0d74..2de8ada3e29 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -15,6 +15,13 @@ class Projects::BranchesController < Projects::ApplicationController
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @repository.branch_names
+ end
+ end
end
def recent
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 606552fa853..d0c4550733c 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
- group: group, group_access: params[:link_group_access]
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
)
redirect_to namespace_project_group_links_path(project.namespace, project)
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 639cf4c0ef2..7c03dcd2e64 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -125,6 +125,10 @@ class Projects::IssuesController < Projects::ApplicationController
render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
end
end
+
+ rescue ActiveRecord::StaleObjectError
+ @conflict = true
+ render :edit
end
def referenced_merge_requests
@@ -208,7 +212,7 @@ class Projects::IssuesController < Projects::ApplicationController
if action_name == 'new'
redirect_to external.new_issue_path
else
- redirect_to external.issues_url
+ redirect_to external.project_path
end
end
@@ -230,7 +234,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential,
- :milestone_id, :due_date, :state_event, :task_num, label_ids: []
+ :milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index d3fe441c4d2..4f5f3b6aa09 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -83,12 +83,22 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def diffs
apply_diff_view_cookie!
- @merge_request_diff = @merge_request.merge_request_diff
+ @merge_request_diff =
+ if params[:diff_id]
+ @merge_request.merge_request_diffs.find(params[:diff_id])
+ else
+ @merge_request.merge_request_diff
+ end
respond_to do |format|
format.html { define_discussion_vars }
format.json do
- @diffs = @merge_request.diffs(diff_options)
+ unless @merge_request_diff.latest?
+ # Disable comments if browsing older version of the diff
+ @diff_notes_disabled = true
+ end
+
+ @diffs = @merge_request_diff.diffs(diff_options)
render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
end
@@ -258,6 +268,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
else
render "edit"
end
+ rescue ActiveRecord::StaleObjectError
+ @conflict = true
+ render :edit
end
def remove_wip
@@ -493,7 +506,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
:title, :assignee_id, :source_project_id, :source_branch,
:target_project_id, :target_branch, :milestone_id,
:state_event, :description, :task_num, :force_remove_source_branch,
- label_ids: []
+ :lock_version, label_ids: []
)
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3435a118964..42a7e5a2c30 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @project.team.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ expires_at: params[:expires_at],
+ current_user: current_user
+ )
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
@@ -94,7 +99,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected
def member_params
- params.require(:project_member).permit(:user_id, :access_level)
+ params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index fc52cd2f367..84d6b106cd7 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -309,7 +309,7 @@ class ProjectsController < Projects::ApplicationController
:issues_tracker_id, :default_branch,
:wiki_enabled, :visibility_level, :import_url, :last_activity_at, :namespace_id, :avatar,
:builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
- :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled
+ :public_builds, :only_allow_merge_if_build_succeeds, :request_access_enabled, :lfs_enabled
)
end
diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb
index 3334b8556df..79eb45568be 100644
--- a/app/finders/move_to_project_finder.rb
+++ b/app/finders/move_to_project_finder.rb
@@ -1,4 +1,6 @@
class MoveToProjectFinder
+ PAGE_SIZE = 50
+
def initialize(user)
@user = user
end
@@ -8,6 +10,10 @@ class MoveToProjectFinder
projects = projects.search(search) if search.present?
projects = projects.excluding_project(from_project)
+ # infinite scroll using offset
+ projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
+ projects = projects.limit(PAGE_SIZE)
+
# to ask for Project#name_with_namespace
projects.includes(namespace: :owner)
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 37bad596a16..06b3e8a9502 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -33,7 +33,7 @@ class TodosFinder
# the project IDs yielded by the todos query thus far
items = by_project(items)
- items.reorder(id: :desc)
+ sort(items)
end
private
@@ -106,6 +106,10 @@ class TodosFinder
params[:type]
end
+ def sort(items)
+ params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
+ end
+
def by_action(items)
if action?
items = items.where(action: to_action_id)
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 78c0b79d2bd..6de25bea654 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -31,6 +31,10 @@ module ApplicationSettingsHelper
current_application_settings.akismet_enabled?
end
+ def koding_enabled?
+ current_application_settings.koding_enabled?
+ end
+
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 9ea03720c1e..e13b7cdd707 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -217,4 +217,12 @@ module BlobHelper
def gitlab_ci_ymls
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
end
+
+ def blob_editor_paths
+ {
+ 'relative-url-root' => Rails.application.config.relative_url_root,
+ 'assets-prefix' => Gitlab::Application.config.assets.prefix,
+ 'blob-language' => @blob && @blob.language.try(:ace_mode)
+ }
+ end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 94df7d131ca..639deb7c521 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -25,6 +25,11 @@ module CiStatusHelper
end
end
+ def ci_status_for_statuseable(subject)
+ status = subject.try(:status) || 'not found'
+ status.humanize
+ end
+
def ci_icon_for_status(status)
icon_name =
case status
@@ -39,9 +44,9 @@ module CiStatusHelper
when 'running'
'icon_status_running'
when 'play'
- return icon('play fw')
+ 'icon_play'
when 'created'
- 'icon_status_pending'
+ 'icon_status_created'
else
'icon_status_cancel'
end
@@ -66,10 +71,10 @@ module CiStatusHelper
Ci::Runner.shared.blank?
end
- def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '')
+ def render_status_with_link(type, status, path = nil, tooltip_placement: 'auto left', cssclass: '', container: 'body')
klass = "ci-status-link ci-status-icon-#{status.dasherize} #{cssclass}"
title = "#{type.titleize}: #{ci_label_for_status(status)}"
- data = { toggle: 'tooltip', placement: tooltip_placement }
+ data = { toggle: 'tooltip', placement: tooltip_placement, container: container }
if path
link_to ci_icon_for_status(status), path,
diff --git a/app/helpers/gitlab_routing_helper.rb b/app/helpers/gitlab_routing_helper.rb
index 5386ddadc62..a322a90cc4e 100644
--- a/app/helpers/gitlab_routing_helper.rb
+++ b/app/helpers/gitlab_routing_helper.rb
@@ -149,4 +149,20 @@ module GitlabRoutingHelper
def resend_invite_group_member_path(group_member, *args)
resend_invite_group_group_member_path(group_member.source, group_member)
end
+
+ # Artifacts
+
+ def artifacts_action_path(path, project, build)
+ action, path_params = path.split('/', 2)
+ args = [project.namespace, project, build, path_params]
+
+ case action
+ when 'download'
+ download_namespace_project_build_artifacts_path(*args)
+ when 'browse'
+ browse_namespace_project_build_artifacts_path(*args)
+ when 'file'
+ file_namespace_project_build_artifacts_path(*args)
+ end
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 47d174361db..b9baeb1d6c4 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -72,6 +72,15 @@ module IssuablesHelper
end
end
+ def issuable_labels_tooltip(labels, limit: 5)
+ first, last = labels.partition.with_index{ |_, i| i < limit }
+
+ label_names = first.collect(&:name)
+ label_names << "and #{last.size} more" unless last.empty?
+
+ label_names.join(', ')
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index eb651e3687e..5d82abfca79 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -23,10 +23,14 @@ module LfsHelper
end
def lfs_download_access?
+ return false unless project.lfs_enabled?
+
project.public? || ci? || (user && user.can?(:download_code, project))
end
def lfs_upload_access?
+ return false unless project.lfs_enabled?
+
user && user.can?(:push_code, project)
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index db6e731c744..a9e175c3f5c 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -98,6 +98,6 @@ module MergeRequestsHelper
end
def merge_request_button_visibility(merge_request, closed)
- return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?)
+ return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 6c1cc6ef072..2b0ff6c0d00 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -25,6 +25,8 @@ module NavHelper
current_path?('merge_requests#commits') ||
current_path?('merge_requests#builds') ||
current_path?('merge_requests#conflicts') ||
+ current_path?('merge_requests#pipelines') ||
+
current_path?('issues#show')
if cookies[:collapsed_gutter] == 'true'
"page-gutter right-sidebar-collapsed"
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 505545fbabb..f07077bd133 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -116,6 +116,17 @@ module ProjectsHelper
license.nickname || license.name
end
+ def last_push_event
+ return unless current_user
+
+ project_ids = [@project.id]
+ if fork = current_user.fork_of(@project)
+ project_ids << fork.id
+ end
+
+ current_user.recent_push(project_ids)
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -176,6 +187,18 @@ module ProjectsHelper
nav_tabs.flatten
end
+ def project_lfs_status(project)
+ if project.lfs_enabled?
+ content_tag(:span, class: 'lfs-enabled') do
+ 'Enabled'
+ end
+ else
+ content_tag(:span, class: 'lfs-disabled') do
+ 'Disabled'
+ end
+ end
+ end
+
def git_user_name
if current_user
current_user.name
@@ -236,6 +259,60 @@ module ProjectsHelper
)
end
+ def add_koding_stack_path(project)
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch || 'master',
+ file_name: '.koding.yml',
+ commit_message: "Add Koding stack script",
+ content: <<-CONTENT.strip_heredoc
+ provider:
+ aws:
+ access_key: '${var.aws_access_key}'
+ secret_key: '${var.aws_secret_key}'
+ resource:
+ aws_instance:
+ #{project.path}-vm:
+ instance_type: t2.nano
+ user_data: |-
+
+ # Created by GitLab UI for :>
+
+ echo _KD_NOTIFY_@Installing Base packages...@
+
+ apt-get update -y
+ apt-get install git -y
+
+ echo _KD_NOTIFY_@Cloning #{project.name}...@
+
+ export KODING_USER=${var.koding_user_username}
+ export REPO_URL=#{root_url}${var.koding_queryString_repo}.git
+ export BRANCH=${var.koding_queryString_branch}
+
+ sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH
+
+ echo _KD_NOTIFY_@#{project.name} cloned.@
+ CONTENT
+ )
+ end
+
+ def koding_project_url(project = nil, branch = nil, sha = nil)
+ if project
+ import_path = "/Home/Stacks/import"
+
+ repo = project.path_with_namespace
+ branch ||= project.default_branch
+ sha ||= project.commit.short_id
+
+ path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"
+
+ return URI.join(current_application_settings.koding_url, path).to_s
+ end
+
+ current_application_settings.koding_url
+ end
+
def contribution_guide_path(project)
if project && contribution_guide = project.repository.contribution_guide
namespace_project_blob_path(
@@ -297,16 +374,6 @@ module ProjectsHelper
namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE')
end
- def last_push_event
- return unless current_user
-
- if fork = current_user.fork_of(@project)
- current_user.recent_push(fork.id)
- else
- current_user.recent_push(@project.id)
- end
- end
-
def readme_cache_key
sha = @project.commit.try(:sha) || 'nil'
[@project.path_with_namespace, sha, "readme"].join('-')
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index c0195713f4a..4549c2e5bb6 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -44,7 +44,7 @@ module SearchHelper
def help_autocomplete
[
{ category: "Help", label: "API Help", url: help_page_path("api/README") },
- { category: "Help", label: "Markdown Help", url: help_page_path("markdown/markdown") },
+ { category: "Help", label: "Markdown Help", url: help_page_path("user/markdown") },
{ category: "Help", label: "Permissions Help", url: help_page_path("user/permissions") },
{ category: "Help", label: "Public Access Help", url: help_page_path("public_access/public_access") },
{ category: "Help", label: "Rake Tasks Help", url: help_page_path("raketasks/README") },
diff --git a/app/helpers/sentry_helper.rb b/app/helpers/sentry_helper.rb
new file mode 100644
index 00000000000..f8cccade15b
--- /dev/null
+++ b/app/helpers/sentry_helper.rb
@@ -0,0 +1,27 @@
+module SentryHelper
+ def sentry_enabled?
+ Rails.env.production? && current_application_settings.sentry_enabled?
+ end
+
+ def sentry_context
+ return unless sentry_enabled?
+
+ if current_user
+ Raven.user_context(
+ id: current_user.id,
+ email: current_user.email,
+ username: current_user.username,
+ )
+ end
+
+ Raven.tags_context(program: sentry_program_context)
+ end
+
+ def sentry_program_context
+ if Sidekiq.server?
+ 'sidekiq'
+ else
+ 'rails'
+ end
+ end
+end
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 790001222f1..271e839692a 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -15,20 +15,9 @@ module TimeHelper
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
- def duration_in_numbers(finished_at, started_at)
- interval = interval_in_seconds(started_at, finished_at)
- time_format = interval < 1.hour ? "%M:%S" : "%H:%M:%S"
+ def duration_in_numbers(duration)
+ time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S"
- Time.at(interval).utc.strftime(time_format)
- end
-
- private
-
- def interval_in_seconds(started_at, finished_at = nil)
- if started_at && finished_at
- finished_at.to_i - started_at.to_i
- elsif started_at
- Time.now.to_i - started_at.to_i
- end
+ Time.at(duration).utc.strftime(time_format)
end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 07f703f205d..c1df4a865f6 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -166,38 +166,44 @@ class Ability
end
def project_abilities(user, project)
- rules = []
key = "/user/#{user.id}/project/#{project.id}"
- RequestStore.store[key] ||= begin
- # Push abilities on the users team role
- rules.push(*project_team_rules(project.team, user))
+ if RequestStore.active?
+ RequestStore.store[key] ||= uncached_project_abilities(user, project)
+ else
+ uncached_project_abilities(user, project)
+ end
+ end
- owner = user.admin? ||
- project.owner == user ||
- (project.group && project.group.has_owner?(user))
+ def uncached_project_abilities(user, project)
+ rules = []
+ # Push abilities on the users team role
+ rules.push(*project_team_rules(project.team, user))
- if owner
- rules.push(*project_owner_rules)
- end
+ owner = user.admin? ||
+ project.owner == user ||
+ (project.group && project.group.has_owner?(user))
- if project.public? || (project.internal? && !user.external?)
- rules.push(*public_project_rules)
+ if owner
+ rules.push(*project_owner_rules)
+ end
- # Allow to read builds for internal projects
- rules << :read_build if project.public_builds?
+ if project.public? || (project.internal? && !user.external?)
+ rules.push(*public_project_rules)
- unless owner || project.team.member?(user) || project_group_member?(project, user)
- rules << :request_access if project.request_access_enabled
- end
- end
+ # Allow to read builds for internal projects
+ rules << :read_build if project.public_builds?
- if project.archived?
- rules -= project_archived_rules
+ unless owner || project.team.member?(user) || project_group_member?(project, user)
+ rules << :request_access if project.request_access_enabled
end
+ end
- rules - project_disabled_features_rules(project)
+ if project.archived?
+ rules -= project_archived_rules
end
+
+ (rules - project_disabled_features_rules(project)).uniq
end
def project_team_rules(team, user)
@@ -349,7 +355,7 @@ class Ability
rules += named_abilities('project_snippet')
end
- unless project.wiki_enabled
+ unless project.has_wiki?
rules += named_abilities('wiki')
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8c19d9dc9c8..246477ffe88 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -55,6 +55,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :akismet_enabled
+ validates :koding_url,
+ presence: true,
+ if: :koding_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -142,13 +146,15 @@ class ApplicationSetting < ActiveRecord::Base
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
+ import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false,
+ koding_enabled: false,
+ koding_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ed056a07a49..23c8de6f650 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -62,6 +62,7 @@ module Ci
status_event: 'enqueue'
)
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+ build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
end
@@ -351,7 +352,7 @@ module Ci
end
def artifacts?
- !artifacts_expired? && artifacts_file.exists?
+ !artifacts_expired? && self[:artifacts_file].present?
end
def artifacts_metadata?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index c360a6ff729..bd1737c7587 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -1,7 +1,7 @@
module Ci
class Pipeline < ActiveRecord::Base
extend Ci::Model
- include Statuseable
+ include HasStatus
self.table_name = 'ci_commits'
@@ -65,8 +65,8 @@ module Ci
end
# ref can't be HEAD or SHA, can only be branch/tag name
- scope :latest_successful_for, ->(ref = default_branch) do
- where(ref: ref).success.order(id: :desc).limit(1)
+ def self.latest_successful_for(ref)
+ where(ref: ref).order(id: :desc).success.first
end
def self.truncate_sha(sha)
@@ -78,8 +78,12 @@ module Ci
CommitStatus.where(pipeline: pluck(:id)).stages
end
+ def self.total_duration
+ where.not(duration: nil).sum(:duration)
+ end
+
def stages_with_latest_statuses
- statuses.latest.order(:stage_idx).group_by(&:stage)
+ statuses.latest.includes(project: :namespace).order(:stage_idx).group_by(&:stage)
end
def project_id
@@ -146,6 +150,10 @@ module Ci
end
end
+ def mark_as_processable_after_stage(stage_idx)
+ builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
+ end
+
def latest?
return false unless ref
commit = project.commit(ref)
@@ -250,7 +258,7 @@ module Ci
end
def update_duration
- self.duration = statuses.latest.duration
+ self.duration = calculate_duration
end
def execute_hooks
diff --git a/app/models/commit.rb b/app/models/commit.rb
index cc413448ce8..817d063e4a2 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -229,7 +229,7 @@ class Commit
def diff_refs
Gitlab::Diff::DiffRefs.new(
- base_sha: self.parent_id || self.sha,
+ base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
head_sha: self.sha
)
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 703ca90edb6..4a628924499 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,5 +1,5 @@
class CommitStatus < ActiveRecord::Base
- include Statuseable
+ include HasStatus
include Importable
self.table_name = 'ci_builds'
@@ -21,15 +21,22 @@ class CommitStatus < ActiveRecord::Base
where(id: max_id.group(:name, :commit_id))
end
+
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
+ scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
+ scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
state_machine :status do
event :enqueue do
transition [:created, :skipped] => :pending
end
+ event :process do
+ transition skipped: :created
+ end
+
event :run do
transition pending: :running
end
@@ -107,13 +114,7 @@ class CommitStatus < ActiveRecord::Base
end
def duration
- duration =
- if started_at && finished_at
- finished_at - started_at
- elsif started_at
- Time.now - started_at
- end
- duration
+ calculate_duration
end
def stuck?
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index 800a16ab246..83f5bc1fa9e 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -59,6 +59,18 @@ module Awardable
true
end
+ def awardable_votes?(name)
+ AwardEmoji::UPVOTE_NAME == name || AwardEmoji::DOWNVOTE_NAME == name
+ end
+
+ def user_can_award?(current_user, name)
+ if user_authored?(current_user)
+ !awardable_votes?(normalize_name(name))
+ else
+ true
+ end
+ end
+
def awarded_emoji?(emoji_name, current_user)
award_emoji.where(name: emoji_name, user: current_user).exists?
end
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
new file mode 100644
index 00000000000..be93435453b
--- /dev/null
+++ b/app/models/concerns/expirable.rb
@@ -0,0 +1,15 @@
+module Expirable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :expired, -> { where('expires_at <= ?', Time.current) }
+ end
+
+ def expires?
+ expires_at.present?
+ end
+
+ def expires_soon?
+ expires_at < 7.days.from_now
+ end
+end
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/has_status.rb
index 5d4b0a86899..f7b8352405c 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/has_status.rb
@@ -1,4 +1,4 @@
-module Statuseable
+module HasStatus
extend ActiveSupport::Concern
AVAILABLE_STATUSES = %w[created pending running success failed canceled skipped]
@@ -35,11 +35,6 @@ module Statuseable
all.pluck(self.status_sql).first
end
- def duration
- duration_array = all.map(&:duration).compact
- duration_array.reduce(:+)
- end
-
def started_at
all.minimum(:started_at)
end
@@ -85,4 +80,14 @@ module Statuseable
def complete?
COMPLETED_STATUSES.include?(status)
end
+
+ private
+
+ def calculate_duration
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index cbae1cd439b..22231b2e0f0 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -87,6 +87,12 @@ module Issuable
User.find(assignee_id_was).update_cache_counts if assignee_id_was
assignee.update_cache_counts if assignee
end
+
+ # We want to use optimistic lock for cases when only title or description are involved
+ # http://api.rubyonrails.org/classes/ActiveRecord/Locking/Optimistic.html
+ def locking_enabled?
+ title_changed? || description_changed?
+ end
end
module ClassMethods
@@ -131,7 +137,10 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
- select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
+ condition_field = "#{table_name}.id"
+ highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
+
+ select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]).
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
end
@@ -159,20 +168,6 @@ module Issuable
grouping_columns
end
-
- private
-
- def highest_label_priority(excluded_labels)
- query = Label.select(Label.arel_table[:priority].minimum).
- joins(:label_links).
- where(label_links: { target_type: name }).
- where("label_links.target_id = #{table_name}.id").
- reorder(nil)
-
- query.where.not(title: excluded_labels) if excluded_labels.present?
-
- query
- end
end
def today?
@@ -201,6 +196,10 @@ module Issuable
end
end
+ def user_authored?(user)
+ user == author
+ end
+
def subscribed_without_subscriptions?(user)
participants(user).include?(user)
end
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 4be6a2f621b..b8dd27a7afe 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -17,6 +17,10 @@ module NoteOnDiff
raise NotImplementedError
end
+ def original_line_code
+ raise NotImplementedError
+ end
+
def diff_attributes
raise NotImplementedError
end
@@ -24,4 +28,8 @@ module NoteOnDiff
def can_be_award_emoji?
false
end
+
+ def to_discussion
+ Discussion.new([self])
+ end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 8b47b9e0abd..1ebecd86af9 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -35,5 +35,19 @@ module Sortable
all
end
end
+
+ private
+
+ def highest_label_priority(object_types, condition_field, excluded_labels: [])
+ query = Label.select(Label.arel_table[:priority].minimum).
+ joins(:label_links).
+ where(label_links: { target_type: object_types }).
+ where("label_links.target_id = #{condition_field}").
+ reorder(nil)
+
+ query.where.not(title: excluded_labels) if excluded_labels.present?
+
+ query
+ end
end
end
diff --git a/app/models/concerns/spammable.rb b/app/models/concerns/spammable.rb
index ce54fe5d3bf..1aa97debe42 100644
--- a/app/models/concerns/spammable.rb
+++ b/app/models/concerns/spammable.rb
@@ -23,7 +23,7 @@ module Spammable
def submittable_as_spam?
if user_agent_detail
- user_agent_detail.submittable?
+ user_agent_detail.submittable? && current_application_settings.akismet_enabled
else
false
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index df2a9e3e84b..a3ac577cf3e 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -52,11 +52,11 @@ module Taskable
end
# Return a string that describes the current state of this Taskable's task
- # list items, e.g. "20 tasks (12 completed, 8 remaining)"
+ # list items, e.g. "12 of 20 tasks completed"
def task_status
return '' if description.blank?
sum = tasks.summary
- "#{sum.item_count} tasks (#{sum.complete_count} completed, #{sum.incomplete_count} remaining)"
+ "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed"
end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index f56c3d74ae3..4442cefc7e9 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -16,6 +16,9 @@ class DiffNote < Note
after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id
+ # We need to do this again, because it's already in `Note`, but is affected by
+ # `update_position` and needs to run after that.
+ before_validation :set_discussion_id
after_save :keep_around_commits
class << self
@@ -57,6 +60,10 @@ class DiffNote < Note
diff_file.position(line) == self.original_position
end
+ def original_line_code
+ self.diff_file.line_code(self.diff_line)
+ end
+
def active?(diff_refs = nil)
return false unless supported?
return true if for_commit?
@@ -100,10 +107,6 @@ class DiffNote < Note
self.noteable.find_diff_discussion(self.discussion_id)
end
- def to_discussion
- Discussion.new([self])
- end
-
private
def supported?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 3fddc084af2..9676bc03470 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -12,6 +12,7 @@ class Discussion
:for_merge_request?,
:line_code,
+ :original_line_code,
:diff_file,
:for_line?,
:active?,
diff --git a/app/models/group.rb b/app/models/group.rb
index 37631b99701..c48869ae465 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,34 +95,40 @@ class Group < Namespace
end
end
- def add_users(user_ids, access_level, current_user = nil)
+ def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
- Member.add_user(self.group_members, user_id, access_level, current_user)
+ Member.add_user(
+ self.group_members,
+ user_id,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
- def add_user(user, access_level, current_user = nil)
- add_users([user], access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ add_users([user], access_level, current_user: current_user, expires_at: expires_at)
end
def add_guest(user, current_user = nil)
- add_user(user, Gitlab::Access::GUEST, current_user)
+ add_user(user, Gitlab::Access::GUEST, current_user: current_user)
end
def add_reporter(user, current_user = nil)
- add_user(user, Gitlab::Access::REPORTER, current_user)
+ add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
end
def add_developer(user, current_user = nil)
- add_user(user, Gitlab::Access::DEVELOPER, current_user)
+ add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
end
def add_master(user, current_user = nil)
- add_user(user, Gitlab::Access::MASTER, current_user)
+ add_user(user, Gitlab::Access::MASTER, current_user: current_user)
end
def add_owner(user, current_user = nil)
- add_user(user, Gitlab::Access::OWNER, current_user)
+ add_user(user, Gitlab::Access::OWNER, current_user: current_user)
end
def has_owner?(user)
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 8e26cbe9835..40277a9b139 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -49,6 +49,10 @@ class LegacyDiffNote < Note
!line.meta? && diff_file.line_code(line) == self.line_code
end
+ def original_line_code
+ self.line_code
+ end
+
# Check if this note is part of an "active" discussion
#
# This will always return true for anything except MergeRequest noteables,
diff --git a/app/models/member.rb b/app/models/member.rb
index 24ab1276ee9..64e0d33fb20 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,6 +1,7 @@
class Member < ActiveRecord::Base
include Sortable
include Importable
+ include Expirable
include Gitlab::Access
attr_accessor :raw_invite_token
@@ -73,7 +74,7 @@ class Member < ActiveRecord::Base
user
end
- def add_user(members, user_id, access_level, current_user = nil)
+ def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
# `user` can be either a User object or an email to be invited
@@ -87,6 +88,7 @@ class Member < ActiveRecord::Base
if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
+ member.expires_at = expires_at
member.save
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 18e97c969d7..ec2d40eb11c 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -34,7 +34,7 @@ class ProjectMember < Member
# :master
# )
#
- def add_users_to_projects(project_ids, user_ids, access, current_user = nil)
+ def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
@@ -50,7 +50,13 @@ class ProjectMember < Member
project = Project.find(project_id)
users.each do |user|
- Member.add_user(project.project_members, user, access_level, current_user)
+ Member.add_user(
+ project.project_members,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 5330a07ee35..62163e74000 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -10,14 +10,16 @@ class MergeRequest < ActiveRecord::Base
belongs_to :source_project, foreign_key: :source_project_id, class_name: "Project"
belongs_to :merge_user, class_name: "User"
- has_one :merge_request_diff, dependent: :destroy
+ has_many :merge_request_diffs, dependent: :destroy
+ has_one :merge_request_diff,
+ -> { order('merge_request_diffs.id DESC') }
has_many :events, as: :target, dependent: :destroy
serialize :merge_params, Hash
- after_create :create_merge_request_diff, unless: :importing?
- after_update :update_merge_request_diff
+ after_create :ensure_merge_request_diff, unless: :importing?
+ after_update :reload_diff_if_branch_changed
delegate :commits, :real_size, to: :merge_request_diff, prefix: nil
@@ -89,13 +91,13 @@ class MergeRequest < ActiveRecord::Base
end
end
- validates :source_project, presence: true, unless: [:allow_broken, :importing?]
+ validates :source_project, presence: true, unless: [:allow_broken, :importing?, :closed_without_fork?]
validates :source_branch, presence: true
validates :target_project, presence: true
validates :target_branch, presence: true
validates :merge_user, presence: true, if: :merge_when_build_succeeds?
- validate :validate_branches, unless: [:allow_broken, :importing?]
- validate :validate_fork
+ validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
+ validate :validate_fork, unless: :closed_without_fork?
scope :by_branch, ->(branch_name) { where("(source_branch LIKE :branch) OR (target_branch LIKE :branch)", branch: branch_name) }
scope :cared, ->(user) { where('assignee_id = :user OR author_id = :user', user: user.id) }
@@ -170,10 +172,10 @@ class MergeRequest < ActiveRecord::Base
end
def diffs(diff_options = nil)
- if self.compare
- self.compare.diffs(diff_options)
+ if compare
+ compare.diffs(diff_options)
else
- Gitlab::Diff::FileCollection::MergeRequest.new(self, diff_options: diff_options)
+ merge_request_diff.diffs(diff_options)
end
end
@@ -184,8 +186,8 @@ class MergeRequest < ActiveRecord::Base
def diff_base_commit
if persisted?
merge_request_diff.base_commit
- elsif diff_start_commit && diff_head_commit
- self.target_project.merge_base_commit(diff_start_sha, diff_head_sha)
+ else
+ branch_merge_base_commit
end
end
@@ -246,6 +248,15 @@ class MergeRequest < ActiveRecord::Base
target_project.repository.commit(target_branch) if target_branch_ref
end
+ def branch_merge_base_commit
+ start_sha = target_branch_sha
+ head_sha = source_branch_sha
+
+ if start_sha && head_sha
+ target_project.merge_base_commit(start_sha, head_sha)
+ end
+ end
+
def target_branch_sha
@target_branch_sha || target_branch_head.try(:sha)
end
@@ -267,16 +278,16 @@ class MergeRequest < ActiveRecord::Base
# Return diff_refs instance trying to not touch the git repository
def diff_sha_refs
if merge_request_diff && merge_request_diff.diff_refs_by_sha?
- return Gitlab::Diff::DiffRefs.new(
- base_sha: merge_request_diff.base_commit_sha,
- start_sha: merge_request_diff.start_commit_sha,
- head_sha: merge_request_diff.head_commit_sha
- )
+ merge_request_diff.diff_refs
else
diff_refs
end
end
+ def branch_merge_base_sha
+ branch_merge_base_commit.try(:sha)
+ end
+
def validate_branches
if target_project == source_project && target_branch == source_branch
errors.add :branch_conflict, "You can not use same project/branch for source and target"
@@ -294,36 +305,49 @@ class MergeRequest < ActiveRecord::Base
def validate_fork
return true unless target_project && source_project
+ return true if target_project == source_project
+ return true unless forked_source_project_missing?
- if target_project == source_project
- true
- else
- # If source and target projects are different
- # we should check if source project is actually a fork of target project
- if source_project.forked_from?(target_project)
- true
- else
- errors.add :validate_fork,
- 'Source project is not a fork of target project'
- end
- end
+ errors.add :validate_fork,
+ 'Source project is not a fork of the target project'
end
- def update_merge_request_diff
+ def closed_without_fork?
+ closed? && forked_source_project_missing?
+ end
+
+ def forked_source_project_missing?
+ return false unless for_fork?
+ return true unless source_project
+
+ !source_project.forked_from?(target_project)
+ end
+
+ def ensure_merge_request_diff
+ merge_request_diff || create_merge_request_diff
+ end
+
+ def create_merge_request_diff
+ merge_request_diffs.create
+ reload_merge_request_diff
+ end
+
+ def reload_merge_request_diff
+ merge_request_diff(true)
+ end
+
+ def reload_diff_if_branch_changed
if source_branch_changed? || target_branch_changed?
reload_diff
end
end
def reload_diff
- return unless merge_request_diff && open?
+ return unless open?
old_diff_refs = self.diff_refs
-
- merge_request_diff.reload_content
-
+ create_merge_request_diff
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
-
new_diff_refs = self.diff_refs
update_diff_notes_positions(
@@ -705,7 +729,9 @@ class MergeRequest < ActiveRecord::Base
end
def pipeline
- @pipeline ||= source_project.pipeline(diff_head_sha, source_branch) if diff_head_sha && source_project
+ return unless diff_head_sha && source_project
+
+ @pipeline ||= source_project.pipeline_for(source_branch, diff_head_sha)
end
def all_pipelines
@@ -777,8 +803,12 @@ class MergeRequest < ActiveRecord::Base
return @conflicts_can_be_resolved_in_ui = false unless has_complete_diff_refs?
begin
- @conflicts_can_be_resolved_in_ui = conflicts.files.each(&:lines)
- rescue Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
+ # Try to parse each conflict. If the MR's mergeable status hasn't been updated,
+ # ensure that we don't say there are conflicts to resolve when there are no conflict
+ # files.
+ conflicts.files.each(&:lines)
+ @conflicts_can_be_resolved_in_ui = conflicts.files.length > 0
+ rescue Rugged::OdbError, Gitlab::Conflict::Parser::ParserError, Gitlab::Conflict::FileCollection::ConflictSideMissing
@conflicts_can_be_resolved_in_ui = false
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 32cc6a3bfea..445179a4487 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -8,8 +8,6 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request
- delegate :source_branch_sha, :target_branch_sha, :target_branch, :source_branch, to: :merge_request, prefix: nil
-
state_machine :state, initial: :empty do
state :collected
state :overflow
@@ -24,12 +22,47 @@ class MergeRequestDiff < ActiveRecord::Base
serialize :st_commits
serialize :st_diffs
- after_create :reload_content, unless: :importing?
- after_save :keep_around_commits, unless: :importing?
+ # All diff information is collected from repository after object is created.
+ # It allows you to override variables like head_commit_sha before getting diff.
+ after_create :save_git_content, unless: :importing?
+
+ def self.select_without_diff
+ select(column_names - ['st_diffs'])
+ end
- def reload_content
+ # Collect information about commits and diff from repository
+ # and save it to the database as serialized data
+ def save_git_content
+ ensure_commits_sha
+ save_commits
reload_commits
- reload_diffs
+ save_diffs
+ keep_around_commits
+ end
+
+ def ensure_commits_sha
+ merge_request.fetch_ref
+ self.start_commit_sha ||= merge_request.target_branch_sha
+ self.head_commit_sha ||= merge_request.source_branch_sha
+ self.base_commit_sha ||= find_base_sha
+ save
+ end
+
+ # Override head_commit_sha to keep compatibility with merge request diff
+ # created before version 8.4 that does not store head_commit_sha in separate db field.
+ def head_commit_sha
+ if persisted? && super.nil?
+ last_commit.try(:sha)
+ else
+ super
+ end
+ end
+
+ # This method will rely on repository branch sha
+ # in case start_commit_sha is nil. Its necesarry for old merge request diff
+ # created before version 8.4 to work
+ def safe_start_commit_sha
+ start_commit_sha || merge_request.target_branch_sha
end
def size
@@ -38,14 +71,11 @@ class MergeRequestDiff < ActiveRecord::Base
def raw_diffs(options = {})
if options[:ignore_whitespace_change]
- @raw_diffs_no_whitespace ||= begin
- compare = Gitlab::Git::Compare.new(
+ @diffs_no_whitespace ||=
+ Gitlab::Git::Compare.new(
repository.raw_repository,
- self.start_commit_sha || self.target_branch_sha,
- self.head_commit_sha || self.source_branch_sha,
- )
- compare.diffs(options)
- end
+ safe_start_commit_sha,
+ head_commit_sha).diffs(options)
else
@raw_diffs ||= {}
@raw_diffs[options] ||= load_diffs(st_diffs, options)
@@ -56,6 +86,11 @@ class MergeRequestDiff < ActiveRecord::Base
@commits ||= load_commits(st_commits || [])
end
+ def reload_commits
+ @commits = nil
+ commits
+ end
+
def last_commit
commits.first
end
@@ -65,55 +100,60 @@ class MergeRequestDiff < ActiveRecord::Base
end
def base_commit
- return unless self.base_commit_sha
+ return unless base_commit_sha
- project.commit(self.base_commit_sha)
+ project.commit(base_commit_sha)
end
def start_commit
- return unless self.start_commit_sha
+ return unless start_commit_sha
- project.commit(self.start_commit_sha)
+ project.commit(start_commit_sha)
end
def head_commit
- return last_commit unless self.head_commit_sha
+ return unless head_commit_sha
+
+ project.commit(head_commit_sha)
+ end
+
+ def diff_refs
+ return unless start_commit_sha || base_commit_sha
- project.commit(self.head_commit_sha)
+ Gitlab::Diff::DiffRefs.new(
+ base_sha: base_commit_sha,
+ start_sha: start_commit_sha,
+ head_sha: head_commit_sha
+ )
end
def diff_refs_by_sha?
base_commit_sha? && head_commit_sha? && start_commit_sha?
end
+ def diffs(diff_options = nil)
+ Gitlab::Diff::FileCollection::MergeRequestDiff.new(self, diff_options: diff_options)
+ end
+
+ def project
+ merge_request.target_project
+ end
+
def compare
@compare ||=
- begin
- # Update ref for merge request
- merge_request.fetch_ref
+ Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ safe_start_commit_sha,
+ head_commit_sha
+ )
+ end
- Gitlab::Git::Compare.new(
- repository.raw_repository,
- self.target_branch_sha,
- self.source_branch_sha
- )
- end
+ def latest?
+ self == merge_request.merge_request_diff
end
private
- # Collect array of Git::Commit objects
- # between target and source branches
- def unmerged_commits
- commits = compare.commits
-
- if commits.present?
- commits = Commit.decorate(commits, merge_request.source_project).reverse
- end
-
- commits
- end
-
def dump_commits(commits)
commits.map(&:to_hash)
end
@@ -122,26 +162,21 @@ class MergeRequestDiff < ActiveRecord::Base
array.map { |hash| Commit.new(Gitlab::Git::Commit.new(hash), merge_request.source_project) }
end
- # Reload all commits related to current merge request from repo
+ # Load all commits related to current merge request diff from repo
# and save it as array of hashes in st_commits db field
- def reload_commits
+ def save_commits
new_attributes = {}
- commit_objects = unmerged_commits
+ commits = compare.commits
- if commit_objects.present?
- new_attributes[:st_commits] = dump_commits(commit_objects)
+ if commits.present?
+ commits = Commit.decorate(commits, merge_request.source_project).reverse
+ new_attributes[:st_commits] = dump_commits(commits)
end
update_columns_serialized(new_attributes)
end
- # Collect array of Git::Diff objects
- # between target and source branches
- def unmerged_diffs
- compare.diffs(Commit.max_diff_options)
- end
-
def dump_diffs(diffs)
if diffs.respond_to?(:map)
diffs.map(&:to_hash)
@@ -162,16 +197,16 @@ class MergeRequestDiff < ActiveRecord::Base
end
end
- # Reload diffs between branches related to current merge request from repo
+ # Load diffs between branches related to current merge request diff from repo
# and save it as array of hashes in st_diffs db field
- def reload_diffs
+ def save_diffs
new_attributes = {}
new_diffs = []
if commits.size.zero?
new_attributes[:state] = :empty
else
- diff_collection = unmerged_diffs
+ diff_collection = compare.diffs(Commit.max_diff_options)
if diff_collection.overflow?
# Set our state to 'overflow' to make the #empty? and #collected?
@@ -188,32 +223,17 @@ class MergeRequestDiff < ActiveRecord::Base
end
new_attributes[:st_diffs] = new_diffs
-
- new_attributes[:start_commit_sha] = self.target_branch_sha
- new_attributes[:head_commit_sha] = self.source_branch_sha
- new_attributes[:base_commit_sha] = branch_base_sha
-
update_columns_serialized(new_attributes)
-
- keep_around_commits
- end
-
- def project
- merge_request.target_project
end
def repository
project.repository
end
- def branch_base_commit
- return unless self.source_branch_sha && self.target_branch_sha
-
- project.merge_base_commit(self.source_branch_sha, self.target_branch_sha)
- end
+ def find_base_sha
+ return unless head_commit_sha && start_commit_sha
- def branch_base_sha
- branch_base_commit.try(:sha)
+ project.merge_base_commit(head_commit_sha, start_commit_sha).try(:sha)
end
def utf8_st_diffs
@@ -248,8 +268,8 @@ class MergeRequestDiff < ActiveRecord::Base
end
def keep_around_commits
- repository.keep_around(target_branch_sha)
- repository.keep_around(source_branch_sha)
- repository.keep_around(branch_base_sha)
+ repository.keep_around(start_commit_sha)
+ repository.keep_around(head_commit_sha)
+ repository.keep_around(base_commit_sha)
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 3bbf5db0b70..b94e3cff2ce 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -223,6 +223,10 @@ class Note < ActiveRecord::Base
end
end
+ def user_authored?(user)
+ user == author
+ end
+
def award_emoji?
can_be_award_emoji? && contains_emoji_only?
end
@@ -259,6 +263,8 @@ class Note < ActiveRecord::Base
def ensure_discussion_id
return unless self.persisted?
+ # Needed in case the SELECT statement doesn't ask for `discussion_id`
+ return unless self.has_attribute?(:discussion_id)
return if self.discussion_id
set_discussion_id
diff --git a/app/models/project.rb b/app/models/project.rb
index 043da030468..e5027af4a0e 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -390,6 +390,13 @@ class Project < ActiveRecord::Base
end
end
+ def lfs_enabled?
+ return false unless Gitlab.config.lfs.enabled
+ return Gitlab.config.lfs.enabled if self[:lfs_enabled].nil?
+
+ self[:lfs_enabled]
+ end
+
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage]
end
@@ -436,7 +443,7 @@ class Project < ActiveRecord::Base
# ref can't be HEAD, can only be branch/tag name or SHA
def latest_successful_builds_for(ref = default_branch)
- latest_pipeline = pipelines.latest_successful_for(ref).first
+ latest_pipeline = pipelines.latest_successful_for(ref)
if latest_pipeline
latest_pipeline.builds.latest.with_artifacts
@@ -471,8 +478,6 @@ class Project < ActiveRecord::Base
end
def reset_cache_and_import_attrs
- update(import_error: nil)
-
ProjectCacheWorker.perform_async(self.id)
self.import_data.destroy if self.import_data
@@ -611,7 +616,10 @@ class Project < ActiveRecord::Base
end
def new_issue_address(author)
- if Gitlab::IncomingEmail.enabled? && author
+ # This feature is disabled for the time being.
+ return nil
+
+ if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode
Gitlab::IncomingEmail.reply_address(
"#{path_with_namespace}+#{author.authentication_token}")
end
@@ -679,6 +687,10 @@ class Project < ActiveRecord::Base
update_column(:has_external_issue_tracker, services.external_issue_trackers.any?)
end
+ def has_wiki?
+ wiki_enabled? || has_external_wiki?
+ end
+
def external_wiki
if has_external_wiki.nil?
cache_has_external_wiki # Populate
@@ -1003,8 +1015,8 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
- def add_user(user, access_level, current_user = nil)
- team.add_user(user, access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
end
def default_branch
@@ -1034,6 +1046,7 @@ class Project < ActiveRecord::Base
"refs/heads/#{branch}",
force: true)
repository.copy_gitattributes(branch)
+ repository.expire_avatar_cache(branch)
reload_default_branch
end
@@ -1094,12 +1107,17 @@ class Project < ActiveRecord::Base
!namespace.share_with_group_lock
end
- def pipeline(sha, ref)
+ def pipeline_for(ref, sha = nil)
+ sha ||= commit(ref).try(:sha)
+
+ return unless sha
+
pipelines.order(id: :desc).find_by(sha: sha, ref: ref)
end
- def ensure_pipeline(sha, ref, current_user = nil)
- pipeline(sha, ref) || pipelines.create(sha: sha, ref: ref, user: current_user)
+ def ensure_pipeline(ref, sha, current_user = nil)
+ pipeline_for(ref, sha) ||
+ pipelines.create(sha: sha, ref: ref, user: current_user)
end
def enable_ci
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index e52a6bd7c84..7613cbdea93 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -1,4 +1,6 @@
class ProjectGroupLink < ActiveRecord::Base
+ include Expirable
+
GUEST = 10
REPORTER = 20
DEVELOPER = 30
@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base
self.class.access_options.key(self.group_access)
end
- private
+ private
def different_group
if self.group && self.project && self.project.group == self.group
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d0a714cd6fc..ab6ea2aae36 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -15,9 +15,9 @@ class ProjectTeam
users, access, current_user = *args
if users.respond_to?(:each)
- add_users(users, access, current_user)
+ add_users(users, access, current_user: current_user)
else
- add_user(users, access, current_user)
+ add_user(users, access, current_user: current_user)
end
end
@@ -33,17 +33,18 @@ class ProjectTeam
member
end
- def add_users(users, access, current_user = nil)
+ def add_users(users, access, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects(
[project.id],
users,
access,
- current_user
+ current_user: current_user,
+ expires_at: expires_at
)
end
- def add_user(user, access, current_user = nil)
- add_users([user], access, current_user)
+ def add_user(user, access, current_user: nil, expires_at: nil)
+ add_users([user], access, current_user: current_user, expires_at: expires_at)
end
# Remove all users from project team
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 2494c266cd2..91bdafdac99 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -277,7 +277,7 @@ class Repository
def cache_keys
%i(size commit_count
readme version contribution_guide changelog
- license_blob license_key gitignore)
+ license_blob license_key gitignore koding_yml)
end
# Keys for data on branch/tag operations.
@@ -553,6 +553,14 @@ class Repository
end
end
+ def koding_yml
+ return nil unless head_exists?
+
+ cache.fetch(:koding_yml) do
+ file_on_head(/\A\.koding\.yml\z/)
+ end
+ end
+
def gitlab_ci_yml
return nil unless head_exists?
@@ -1057,7 +1065,7 @@ class Repository
@avatar ||= cache.fetch(:avatar) do
AVATAR_FILES.find do |file|
- blob_at_branch('master', file)
+ blob_at_branch(root_ref, file)
end
end
end
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 8d7a5965aa1..6ae9956ade5 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,4 +1,6 @@
class Todo < ActiveRecord::Base
+ include Sortable
+
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
@@ -41,6 +43,23 @@ class Todo < ActiveRecord::Base
after_save :keep_around_commit
+ class << self
+ def sort(method)
+ method == "priority" ? order_by_labels_priority : order_by(method)
+ end
+
+ # Order by priority depending on which issue/merge request the Todo belongs to
+ # Todos with highest priority first then oldest todos
+ # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
+ def order_by_labels_priority
+ highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql
+
+ select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
+ order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
+ order('todos.created_at')
+ end
+ end
+
def build_failed?
action == BUILD_FAILED
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 48e83ab7e56..ad3cfbc03e4 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -489,10 +489,10 @@ class User < ActiveRecord::Base
(personal_projects.count.to_f / projects_limit) * 100
end
- def recent_push(project_id = nil)
+ def recent_push(project_ids = nil)
# Get push events not earlier than 2 hours ago
events = recent_events.code_push.where("created_at > ?", Time.now - 2.hours)
- events = events.where(project_id: project_id) if project_id
+ events = events.where(project_id: project_ids) if project_ids
# Use the latest event that has not been pushed or merged recently
events.recent.find do |event|
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 435a8c6e681..34efd09ed9f 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -36,7 +36,12 @@ module Boards
end
def set_state
- params[:state] = list.done? ? 'closed' : 'opened'
+ params[:state] =
+ case list.list_type.to_sym
+ when :backlog then 'opened'
+ when :done then 'closed'
+ else 'all'
+ end
end
def board_label_ids
diff --git a/app/services/boards/lists/create_service.rb b/app/services/boards/lists/create_service.rb
index 5cb408b9d20..b1887820bd4 100644
--- a/app/services/boards/lists/create_service.rb
+++ b/app/services/boards/lists/create_service.rb
@@ -3,7 +3,10 @@ module Boards
class CreateService < Boards::BaseService
def execute
List.transaction do
- create_list_at(next_position)
+ label = project.labels.find(params[:label_id])
+ position = next_position
+
+ create_list(label, position)
end
end
@@ -14,8 +17,8 @@ module Boards
max_position.nil? ? 0 : max_position.succ
end
- def create_list_at(position)
- board.lists.create(params.merge(list_type: :label, position: position))
+ def create_list(label, position)
+ board.lists.create(label: label, list_type: :label, position: position)
end
end
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 6f7610d42ba..f049ed628db 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -34,7 +34,7 @@ module Ci
end
def process_build(build, current_status)
- return false unless Statuseable::COMPLETED_STATUSES.include?(current_status)
+ return false unless HasStatus::COMPLETED_STATUSES.include?(current_status)
if valid_statuses_for_when(build.when).include?(current_status)
build.enqueue
diff --git a/app/services/ci/web_hook_service.rb b/app/services/ci/web_hook_service.rb
deleted file mode 100644
index 92e6df442b4..00000000000
--- a/app/services/ci/web_hook_service.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-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/issuable_base_service.rb b/app/services/issuable_base_service.rb
index e06c37c323e..4c8d93999a7 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -45,6 +45,7 @@ class IssuableBaseService < BaseService
unless can?(current_user, ability, project)
params.delete(:milestone_id)
+ params.delete(:labels)
params.delete(:add_label_ids)
params.delete(:remove_label_ids)
params.delete(:label_ids)
@@ -72,6 +73,7 @@ class IssuableBaseService < BaseService
filter_labels_in_param(:add_label_ids)
filter_labels_in_param(:remove_label_ids)
filter_labels_in_param(:label_ids)
+ find_or_create_label_ids
end
def filter_labels_in_param(key)
@@ -80,6 +82,17 @@ class IssuableBaseService < BaseService
params[key] = project.labels.where(id: params[key]).pluck(:id)
end
+ def find_or_create_label_ids
+ labels = params.delete(:labels)
+ return unless labels
+
+ params[:label_ids] = labels.split(",").map do |label_name|
+ project.labels.create_with(color: Label::DEFAULT_COLOR)
+ .find_or_create_by(title: label_name.strip)
+ .id
+ end
+ end
+
def process_label_ids(attributes, existing_label_ids: nil)
label_ids = attributes.delete(:label_ids)
add_label_ids = attributes.delete(:add_label_ids)
@@ -162,7 +175,12 @@ class IssuableBaseService < BaseService
if params.present? && update_issuable(issuable, params)
issuable.reset_events_cache
- handle_common_system_notes(issuable, old_labels: old_labels)
+
+ # We do not touch as it will affect a update on updated_at field
+ ActiveRecord::Base.no_touching do
+ handle_common_system_notes(issuable, old_labels: old_labels)
+ end
+
handle_changes(issuable, old_labels: old_labels, old_mentioned_users: old_mentioned_users)
issuable.create_new_cross_references!(current_user)
execute_hooks(issuable, 'update')
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
new file mode 100644
index 00000000000..ca9db59cac7
--- /dev/null
+++ b/app/services/members/authorized_destroy_service.rb
@@ -0,0 +1,19 @@
+module Members
+ class AuthorizedDestroyService < BaseService
+ attr_accessor :member, :user
+
+ def initialize(member, user = nil)
+ @member, @user = member, user
+ end
+
+ def execute
+ return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
+
+ member.destroy
+
+ if member.request? && member.user != user
+ notification_service.decline_access_request(member)
+ end
+ end
+ end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 9e3f6af628d..9a2bf82ef51 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -11,12 +11,7 @@ module Members
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
-
- member.destroy
-
- if member.request? && member.user != current_user
- notification_service.decline_access_request(member)
- end
+ AuthorizedDestroyService.new(member, current_user).execute
end
end
end
diff --git a/app/services/merge_requests/resolve_service.rb b/app/services/merge_requests/resolve_service.rb
index adc71b0c2bc..19caa038c44 100644
--- a/app/services/merge_requests/resolve_service.rb
+++ b/app/services/merge_requests/resolve_service.rb
@@ -1,11 +1,14 @@
module MergeRequests
class ResolveService < MergeRequests::BaseService
- attr_accessor :conflicts, :rugged, :merge_index
+ attr_accessor :conflicts, :rugged, :merge_index, :merge_request
def execute(merge_request)
@conflicts = merge_request.conflicts
@rugged = project.repository.rugged
@merge_index = conflicts.merge_index
+ @merge_request = merge_request
+
+ fetch_their_commit!
conflicts.files.each do |file|
write_resolved_file_to_index(file, params[:sections])
@@ -27,5 +30,21 @@ module MergeRequests
merge_index.add(path: our_path, oid: rugged.write(new_file, :blob), mode: file.our_mode)
merge_index.conflict_remove(our_path)
end
+
+ # If their commit (in the target project) doesn't exist in the source project, it
+ # can't be a parent for the merge commit we're about to create. If that's the case,
+ # fetch the target branch ref into the source project so the commit exists in both.
+ #
+ def fetch_their_commit!
+ return if rugged.include?(conflicts.their_commit.oid)
+
+ random_string = SecureRandom.hex
+
+ project.repository.fetch_ref(
+ merge_request.target_project.repository.path_to_repo,
+ "refs/heads/#{merge_request.target_branch}",
+ "refs/tmp/#{random_string}/head"
+ )
+ end
end
end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index 30c5f24988c..398ec47f0ea 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -11,6 +11,10 @@ module MergeRequests
params.except!(:target_project_id)
params.except!(:source_branch)
+ if merge_request.closed_without_fork?
+ params.except!(:target_branch, :force_remove_source_branch)
+ end
+
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
update(merge_request)
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 546a8f11330..0c8446e7c3d 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -269,11 +269,11 @@ module SystemNoteService
#
# Example Note text:
#
- # "mentioned in #1"
+ # "Mentioned in #1"
#
- # "mentioned in !2"
+ # "Mentioned in !2"
#
- # "mentioned in 54f7727c"
+ # "Mentioned in 54f7727c"
#
# See cross_reference_note_content.
#
@@ -308,7 +308,7 @@ module SystemNoteService
# Check if a cross-reference is disallowed
#
- # This method prevents adding a "mentioned in !1" note on every single commit
+ # This method prevents adding a "Mentioned in !1" note on every single commit
# in a merge request. Additionally, it prevents the creation of references to
# external issues (which would fail).
#
@@ -417,7 +417,7 @@ module SystemNoteService
end
def cross_reference_note_prefix
- 'mentioned in '
+ 'Mentioned in '
end
def cross_reference_note_content(gfm_reference)
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index e0ccb654590..2aab8c736d6 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -148,7 +148,8 @@ class TodoService
def mark_todos_as_done_by_ids(ids, current_user)
todos = current_user.todos.where(id: ids)
- marked_todos = todos.update_all(state: :done)
+ # Only return those that are not really on that state
+ marked_todos = todos.where.not(state: :done).update_all(state: :done)
current_user.update_todos_count_cache
marked_todos
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index 92e2dae4842..9175b3d3f96 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -13,7 +13,7 @@
.col-sm-10
= f.text_area :description, class: "form-control", rows: 10
.hint
- Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('markdown/markdown'), target: '_blank'}.
+ Description parsed with #{link_to "GitLab Flavored Markdown", help_page_path('user/markdown'), target: '_blank'}.
.form-group
= f.label :logo, class: 'control-label'
.col-sm-10
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index c7fd344eea2..d929364fc96 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -388,6 +388,25 @@
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+ %fieldset
+ %legend Koding
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :koding_enabled do
+ = f.check_box :koding_enabled
+ Enable Koding
+ .form-group
+ = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .help-block
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding administration documentation", help_page_path("administration/integration/koding")
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml
index 89d7a40d6b0..107fc25244a 100644
--- a/app/views/admin/background_jobs/_head.html.haml
+++ b/app/views/admin/background_jobs/_head.html.haml
@@ -1,22 +1,24 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- = nav_link(controller: :system_info) do
- = link_to admin_system_info_path, title: 'System Info' do
- %span
- System Info
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- %span
- Background Jobs
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs' do
- %span
- Logs
- = nav_link(controller: :health_check) do
- = link_to admin_health_check_path, title: 'Health Check' do
- %span
- Health Check
- = nav_link(controller: :requests_profiles) do
- = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
- %span
- Requests Profiles
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :system_info) do
+ = link_to admin_system_info_path, title: 'System Info' do
+ %span
+ System Info
+ = nav_link(controller: :background_jobs) do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
+ %span
+ Background Jobs
+ = nav_link(controller: :logs) do
+ = link_to admin_logs_path, title: 'Logs' do
+ %span
+ Logs
+ = nav_link(controller: :health_check) do
+ = link_to admin_health_check_path, title: 'Health Check' do
+ %span
+ Health Check
+ = nav_link(controller: :requests_profiles) do
+ = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
+ %span
+ Requests Profiles
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 352adbedee4..f29d9c94441 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -51,7 +51,7 @@
- if build.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(build.finished_at, build.started_at)
+ = duration_in_numbers(build.duration)
- if build.finished_at
%p.finished-at
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index b74da64f82e..c91ab4cb946 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -1,26 +1,28 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- %span
- Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- %span
- Projects
- = nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- %span
- Users
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
- %span
- Builds
- = nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path, title: 'Runners' do
- %span
- Runners
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview' do
+ %span
+ Overview
+ = nav_link(controller: [:admin, :projects]) do
+ = link_to admin_namespaces_projects_path, title: 'Projects' do
+ %span
+ Projects
+ = nav_link(controller: :users) do
+ = link_to admin_users_path, title: 'Users' do
+ %span
+ Users
+ = nav_link(controller: :groups) do
+ = link_to admin_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link path: 'builds#index' do
+ = link_to admin_builds_path, title: 'Builds' do
+ %span
+ Builds
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path, title: 'Runners' do
+ %span
+ Runners
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index b2c607361b3..6c7c3c48604 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -73,6 +73,12 @@
%span.light last commit:
%strong
= last_commit(@project)
+
+ %li
+ %span.light Git LFS status:
+ %strong
+ = project_lfs_status(@project)
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- else
%li
%span.light repository:
diff --git a/app/views/admin/system_info/show.html.haml b/app/views/admin/system_info/show.html.haml
index 6956e5ab795..bfc6142067a 100644
--- a/app/views/admin/system_info/show.html.haml
+++ b/app/views/admin/system_info/show.html.haml
@@ -9,12 +9,20 @@
.light-well
%h4 CPU
.data
- %h1= "#{@cpus} cores"
+ - if @cpus
+ %h1= "#{@cpus.length} cores"
+ - else
+ = icon('warning', class: 'text-warning')
+ Unable to collect CPU info
.col-sm-4
.light-well
%h4 Memory
.data
- %h1= "#{number_to_human_size(@mem_used)} / #{number_to_human_size(@mem_total)}"
+ - if @memory
+ %h1= "#{number_to_human_size(@memory.active_bytes)} / #{number_to_human_size(@memory.total_bytes)}"
+ - else
+ = icon('warning', class: 'text-warning')
+ Unable to collect memory info
.col-sm-4
.light-well
%h4 Disks
diff --git a/app/views/ci/lints/show.html.haml b/app/views/ci/lints/show.html.haml
index 0044d779c31..889086c62b1 100644
--- a/app/views/ci/lints/show.html.haml
+++ b/app/views/ci/lints/show.html.haml
@@ -1,3 +1,6 @@
+- page_title "CI Lint"
+- page_description "Validate your GitLab CI configuration file"
+
%h2 Check your .gitlab-ci.yml
%hr
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 4e340b6ec16..d320d3bcc1e 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -43,6 +43,25 @@
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Action'})
+ .pull-right
+ .dropdown.inline.prepend-left-10
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_recently_created
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %li
+ = link_to todos_filter_path(sort: sort_value_priority) do
+ = sort_title_priority
+ = link_to todos_filter_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to todos_filter_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
+
+
.prepend-top-default
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index b2e55f7647a..3a95a652810 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,7 +7,7 @@
.diff-content.code.js-syntax-highlight
%table
- - discussions = { discussion.line_code => discussion }
+ - discussions = { discussion.original_line_code => discussion }
= render partial: "projects/diffs/line",
collection: discussion.truncated_diff_lines,
as: :line,
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index 57f6e7e0612..b8248a80a27 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -24,7 +24,7 @@
- else
= sort_title_recently_created
%b.caret
- %ul.dropdown-menu
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
= sort_title_recently_created
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 9bb9f962177..2fb3190ab11 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this group and all of its projects.
+
.form-actions
= f.submit 'Add users to group', class: "btn btn-create"
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index 90f362c052b..f789796e942 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -17,7 +17,7 @@
.panel-heading
%strong #{@group.name}
group members
- %span.badge= @members.size
+ %span.badge= @members.total_count
.controls
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index da71de4cd1e..742f9d7a433 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index 85e188d6f8b..d16bd61b779 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -549,4 +549,4 @@
%li wiki page
%li help page
- You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("markdown/markdown")}.
+ You can check how markdown rendered at #{link_to 'Markdown help page', help_page_path("user/markdown")}.
diff --git a/app/views/import/gitorious/status.html.haml b/app/views/import/gitorious/status.html.haml
deleted file mode 100644
index ed3afb0ce33..00000000000
--- a/app/views/import/gitorious/status.html.haml
+++ /dev/null
@@ -1,54 +0,0 @@
-- page_title "Gitorious import"
-- header_title "Projects", root_path
-%h3.page-title
- %i.icon-gitorious.icon-gitorious-big
- Import projects from Gitorious.org
-
-%p.light
- Select projects you want to import.
-%hr
-%p
- = button_tag class: "btn btn-import btn-success js-import-all" do
- Import all projects
- = icon("spinner spin", class: "loading-icon")
-
-.table-responsive
- %table.table.import-jobs
- %colgroup.import-jobs-from-col
- %colgroup.import-jobs-to-col
- %colgroup.import-jobs-status-col
- %thead
- %tr
- %th From Gitorious.org
- %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
- = link_to project.import_source, "https://gitorious.org/#{project.import_source}", target: "_blank"
- %td
- = 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
- = link_to repo.full_name, "https://gitorious.org/#{repo.full_name}", target: "_blank"
- %td.import-target
- = repo.full_name
- %td.import-actions.job-status
- = button_tag class: "btn btn-import js-add-to-import" do
- Import
- = icon("spinner spin", class: "loading-icon")
-
-.js-importer-status{ data: { jobs_import_path: "#{jobs_import_gitorious_path}", import_path: "#{import_gitorious_path}" } }
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
new file mode 100644
index 00000000000..65887aacbaf
--- /dev/null
+++ b/app/views/koding/index.html.haml
@@ -0,0 +1,6 @@
+.row-content-block.second-block.center
+ %p
+ = icon('circle', class: 'cgreen')
+ Integration is active for
+ = link_to koding_project_url, target: '_blank' do
+ #{current_application_settings.koding_url}
diff --git a/app/views/layouts/koding.html.haml b/app/views/layouts/koding.html.haml
new file mode 100644
index 00000000000..22319bba745
--- /dev/null
+++ b/app/views/layouts/koding.html.haml
@@ -0,0 +1,5 @@
+- page_title "Koding"
+- page_description "Koding Dashboard"
+- header_title "Koding", koding_path
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 3a14751ea8e..67f558c854b 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -12,6 +12,11 @@
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
%span
Activity
+ - if koding_enabled?
+ = nav_link(controller: :koding) do
+ = link_to koding_path, title: 'Koding' do
+ %span
+ Koding
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 1d3b8fc3683..f7012595a5a 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -65,7 +65,7 @@
Graphs
- if project_nav_tab? :issues
- = nav_link(controller: [:issues, :labels, :milestones]) do
+ = nav_link(controller: [:issues, :labels, :milestones, :boards]) do
= link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
%span
Issues
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 7b0621f9401..680e95ac6b5 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,4 +1,7 @@
- page_title "Edit", @blob.path, @ref
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
- if @conflict
.alert.alert-danger
@@ -16,14 +19,10 @@
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
-
-:javascript
- blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
- new NewCommitForm($('.js-edit-blob-form'))
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index c952bc7e5db..b6ed9518c48 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,17 +1,16 @@
- page_title "New File", @path.presence, @ref
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
%h3.page-title
New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
-
-:javascript
- blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}")
- new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index f8ebf397ee2..6b4bfe0c354 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -11,7 +11,6 @@
.board-inner
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
- = icon("align-justify", class: "board-mobile-handle js-board-drag-handle", "v-if" => "(!disabled && !list.preset)")
{{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" }
{{ list.issues.length }}
@@ -22,11 +21,6 @@
%button.board-delete.has-tooltip.pull-right{ type: "button", title: "Delete list", "aria-label" => "Delete list", data: { placement: "bottom" }, "@click.stop" => "deleteBoard" }
= icon("trash")
= icon("spinner spin", class: "board-header-loading-spinner pull-right", "v-show" => "list.loadingMore")
- .board-inner-container.board-search-container{ "v-if" => "list.canSearch()" }
- %input.form-control{ type: "text", placeholder: "Search issues", "v-model" => "query", "debounce" => "250" }
- = icon("search", class: "board-search-icon", "v-show" => "!query")
- %button.board-search-clear-btn{ type: "button", role: "button", "aria-label" => "Clear search", "@click" => "query = ''", "v-show" => "query" }
- = icon("times", class: "board-search-clear")
%board-list{ "inline-template" => true,
"v-if" => "list.type !== 'blank'",
":list" => "list",
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index b20c23f6b8e..e8b60b54d80 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -9,7 +9,6 @@
"track-by" => "id" }
%li.card{ ":class" => "{ 'user-can-drag': !disabled }",
":index" => "index" }
- = icon("align-justify", class: "board-mobile-handle js-card-drag-handle", "v-if" => "!disabled")
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id",
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 6192ccb710b..808e6b95746 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -27,6 +27,8 @@
= link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
Compare
+ = render 'projects/buttons/download', project: @project, ref: branch.name
+
- if can_remove_branch?(@project, branch.name)
= link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 58f43ecb5d5..5f5e071eb40 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,4 +1,42 @@
-- unless @project.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 has-tooltip', data: {container: "body"}, rel: 'nofollow', title: "Download ZIP" do
- = icon('download')
+- if !project.empty_repo? && can?(current_user, :download_code, project)
+ %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'}
+ .dropdown.inline
+ %button.btn{ 'data-toggle' => 'dropdown' }
+ = icon('download')
+ %span.caret
+ %span.sr-only
+ Select Archive Format
+ %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
+ %li.dropdown-header Source code
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download zip
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.gz
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar.bz2
+ %li
+ = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar'), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download tar
+
+ - pipeline = project.pipelines.latest_successful_for(ref)
+ - if pipeline
+ - artifacts = pipeline.builds.latest.with_artifacts
+ - if artifacts.any?
+ %li.dropdown-header Artifacts
+ - unless pipeline.latest?
+ - latest_pipeline = project.pipeline_for(ref)
+ %li
+ .unclickable= ci_status_for_statuseable(latest_pipeline)
+ %li.dropdown-header Previous Artifacts
+ - artifacts.each do |job|
+ %li
+ = link_to latest_succeeded_namespace_project_artifacts_path(project.namespace, project, ref, 'download', job: job.name), rel: 'nofollow' do
+ %i.fa.fa-download
+ %span Download '#{job.name}'
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
new file mode 100644
index 00000000000..fdc80d44253
--- /dev/null
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -0,0 +1,7 @@
+- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch)
+ - if @repository.koding_yml
+ = link_to koding_project_url(@project), class: 'btn', target: '_blank' do
+ Run in IDE (Koding)
+ - else
+ = link_to add_koding_stack_path(@project), class: 'btn' do
+ Set Up Koding
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 91081435220..1fdf32466f2 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -63,7 +63,7 @@
- if build.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(build.finished_at, build.started_at)
+ = duration_in_numbers(build.duration)
- if build.finished_at
%p.finished-at
= icon("calendar")
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 04cbd0c3591..36fb0300aeb 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,14 +1,15 @@
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
%li.build{class: ("playable" if is_playable)}
+ .curve
.build-content
- if is_playable
= link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, title: 'Play' do
= render_status_with_link('build', 'play')
- = subject.name
+ %span.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
= render_status_with_link('build', subject.status)
- = subject.name
+ %span.ci-status-text= subject.name
- else
= render_status_with_link('build', subject.status)
= ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index be387201f8d..b119f6edf14 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -1,23 +1,23 @@
- status = pipeline.status
%tr.commit
%td.commit-link
- = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- if defined?(status_icon_only) && status_icon_only
= ci_icon_for_status(status)
- else
= ci_status_with_icon(status)
%td
.branch-commit
- = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id) do
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
%span ##{pipeline.id}
- if pipeline.ref
- unless defined?(hide_branch) && hide_branch
.icon-container
= pipeline.tag? ? icon('tag') : icon('code-fork')
- = link_to pipeline.ref, namespace_project_commits_path(@project.namespace, @project, pipeline.ref), class: "monospace branch-name"
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
.icon-container
= custom_icon("icon_commit")
- = link_to pipeline.short_sha, namespace_project_commit_path(@project.namespace, @project, pipeline.sha), class: "commit-id monospace"
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
- if pipeline.latest?
%span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
- if pipeline.triggered?
@@ -30,7 +30,7 @@
%p.commit-title
- if commit = pipeline.commit
= author_avatar(commit, size: 20)
- = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message"
+ = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
@@ -41,17 +41,17 @@
- status = stages_status[stage]
- tooltip = "#{stage.titleize}: #{status || 'not found'}"
- if status
- = link_to namespace_project_pipeline_path(@project.namespace, @project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id, anchor: stage), class: "has-tooltip ci-status-icon-#{status}", title: tooltip do
= ci_icon_for_status(status)
- else
.light.has-tooltip{ title: tooltip }
\-
%td
- - if pipeline.started_at && pipeline.finished_at
+ - if pipeline.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(pipeline.finished_at, pipeline.started_at)
+ = duration_in_numbers(pipeline.duration)
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
@@ -71,7 +71,7 @@
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
- = link_to play_namespace_project_build_path(@project.namespace, @project, build), method: :post, rel: 'nofollow' do
+ = link_to play_namespace_project_build_path(pipeline.project.namespace, pipeline.project, build), method: :post, rel: 'nofollow' do
= icon("play")
%span= build.name.humanize
- if artifacts.present?
@@ -82,15 +82,15 @@
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
- = link_to download_namespace_project_build_artifacts_path(@project.namespace, @project, build), rel: 'nofollow' do
+ = link_to download_namespace_project_build_artifacts_path(pipeline.project.namespace, pipeline.project, build), rel: 'nofollow' do
= icon("download")
%span Download '#{build.name}' artifacts
- - if can?(current_user, :update_pipeline, @project)
+ - if can?(current_user, :update_pipeline, pipeline.project)
.cancel-retry-btns.inline
- if pipeline.retryable?
- = link_to retry_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
+ = link_to retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn has-tooltip', title: "Retry", method: :post do
= icon("repeat")
- if pipeline.cancelable?
- = link_to cancel_namespace_project_pipeline_path(@project.namespace, @project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
+ = link_to cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-remove has-tooltip', title: "Cancel", method: :post do
= icon("remove")
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index d9b800a4ded..e4cd55b9f7a 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -17,7 +17,9 @@
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
- = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch"
+ = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch'
+ = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }})
+
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
diff --git a/app/views/projects/commit/_ci_stage.html.haml b/app/views/projects/commit/_ci_stage.html.haml
index 9d925cacc0d..6bb900e3fc1 100644
--- a/app/views/projects/commit/_ci_stage.html.haml
+++ b/app/views/projects/commit/_ci_stage.html.haml
@@ -8,8 +8,8 @@
- if stage
&nbsp;
= stage.titleize
- = render statuses.latest.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
- = render statuses.retried.ordered, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
+ = render statuses.latest_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, allow_retry: true
+ = render statuses.retried_ci_stages, coverage: @project.build_coverage_enabled?, stage: false, ref: false, retried: true
%tr
%td{colspan: 10}
&nbsp;
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 14adee7a6e9..29d767e7769 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -58,9 +58,8 @@
= ci_icon_for_status(@commit.status)
%span.ci-status-label
= ci_label_for_status(@commit.status)
- - if @commit.pipelines.duration
- in
- = time_interval_in_words @commit.pipelines.duration
+ in
+ = time_interval_in_words @commit.pipelines.total_duration
.commit-box.content-block
%h3.commit-title
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 29f4ef8f49e..f41a11a056d 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -10,7 +10,10 @@
%th Commit
- pipelines.stages.each do |stage|
%th.stage
- %span.has-tooltip{ title: "#{stage.titleize}" }
+ - if stage.titleize.length > 12
+ %span.has-tooltip{ title: "#{stage.titleize}" }
+ = stage.titleize
+ - else
= stage.titleize
%th
%th
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 61152649907..4d1ee1c5318 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,8 +1,5 @@
.scrolling-tabs-container.sub-nav-scroll
- .fade-left
- = icon('angle-left')
- .fade-right
- = icon('angle-right')
+ = render 'shared/nav_scroll'
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index b282aa52b25..836c6d7b83f 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -80,6 +80,16 @@
%strong Snippets
%br
%span.descr Share code pastes with others out of git repository
+ - if Gitlab.config.lfs.enabled && current_user.admin?
+ .form-group
+ .checkbox
+ = f.label :lfs_enabled do
+ = f.check_box :lfs_enabled, checked: @project.lfs_enabled?
+ %strong LFS
+ %br
+ %span.descr
+ Git Large File Storage
+ = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- if Gitlab.config.registry.enabled
.form-group
.checkbox
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 584c0fa18ae..31d40f6ad03 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -3,7 +3,7 @@
- if subject.target_url
- link_to subject.target_url do
= render_status_with_link('commit status', subject.status)
- = subject.name
+ %span.ci-status-text= subject.name
- else
= render_status_with_link('commit status', subject.status)
- = subject.name
+ %span.ci-status-text= subject.name
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 45e51389c00..a231d684559 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,16 +1,18 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
- - content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/chart.js')
- = page_specific_javascript_tag('graphs/graphs_bundle.js')
- = nav_link(action: :show) do
- = link_to 'Contributors', namespace_project_graph_path
- = nav_link(action: :commits) do
- = link_to 'Commits', commits_namespace_project_graph_path
- = nav_link(action: :languages) do
- = link_to 'Languages', languages_namespace_project_graph_path
- - if @project.builds_enabled?
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/chart.js')
+ = page_specific_javascript_tag('graphs/graphs_bundle.js')
+ = nav_link(action: :show) do
+ = link_to 'Contributors', namespace_project_graph_path
+ = nav_link(action: :commits) do
+ = link_to 'Commits', commits_namespace_project_graph_path
+ = nav_link(action: :languages) do
+ = link_to 'Languages', languages_namespace_project_graph_path
+ - if @project.builds_enabled?
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index 2b904544f28..ca700cb3a3b 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -17,6 +17,13 @@
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
%span.caret
+ .form-group
+ = label_tag :expires_at, 'Access expiration date', class: 'label-light'
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, all users in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
@@ -35,6 +42,10 @@
= group.name
%br
up to #{group_link.human_access}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.pull-right
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
%span.sr-only disable sharing
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index b6cb559afcb..f88b33018d0 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -1,30 +1,32 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
- = nav_link(controller: :issues) do
- = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
- %span
- Issues
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
+ = nav_link(controller: :issues) do
+ = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
+ %span
+ Issues
- = nav_link(controller: :boards) do
- = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
- %span
- Board
+ = nav_link(controller: :boards) do
+ = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
+ %span
+ Board
- - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
- = nav_link(controller: :merge_requests) do
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
- %span
- Merge Requests
+ - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+ %span
+ Merge Requests
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
- %span
- Labels
+ - if project_nav_tab? :labels
+ = nav_link(controller: :labels) do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+ %span
+ Labels
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
- %span
- Milestones
+ - if project_nav_tab? :milestones
+ = nav_link(controller: :milestones) do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+ %span
+ Milestones \ No newline at end of file
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 6ea9f612d13..a8eeab3e55e 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -5,7 +5,7 @@
- @related_branches.each do |branch|
%li
- target = @project.repository.find_branch(branch).target
- - pipeline = @project.pipeline(target.sha, branch) if target
+ - pipeline = @project.pipeline_for(branch, target.sha) if target
- if pipeline
%span.related-branch-ci-status
= render_pipeline_status(pipeline)
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index f8025fc1dbe..9d8b4cc56be 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -16,6 +16,9 @@
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
+ - if koding_enabled? && @repository.koding_yml
+ = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+ Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
diff --git a/app/views/projects/merge_requests/show/_diffs.html.haml b/app/views/projects/merge_requests/show/_diffs.html.haml
index 013b05628fa..99c71e1454a 100644
--- a/app/views/projects/merge_requests/show/_diffs.html.haml
+++ b/app/views/projects/merge_requests/show/_diffs.html.haml
@@ -1,4 +1,5 @@
- if @merge_request_diff.collected?
+ = render 'projects/merge_requests/show/versions'
= render "projects/diffs/diffs", diffs: @diffs
- elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
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 098ce19da21..e35291dff7d 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -1,3 +1,7 @@
+- if @merge_request.closed_without_fork?
+ .alert.alert-danger
+ %p The source project of this merge request has been removed.
+
.clearfix.detail-page-header
.issuable-header
.issuable-status-box.status-box{ class: status_box_class(@merge_request) }
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
new file mode 100644
index 00000000000..2da70ce7137
--- /dev/null
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -0,0 +1,31 @@
+- merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+
+- if merge_request_diffs.size > 1
+ .mr-version-switch
+ Version:
+ %span.dropdown.inline
+ %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
+ %strong.monospace<
+ - if @merge_request_diff.latest?
+ #{"latest"}
+ - else
+ #{@merge_request_diff.head_commit.short_id}
+ %span.caret
+ %ul.dropdown-menu.dropdown-menu-selectable
+ - merge_request_diffs.each do |merge_request_diff|
+ %li
+ = link_to diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, diff_id: merge_request_diff.id), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ %strong.monospace
+ #{merge_request_diff.head_commit.short_id}
+ %br
+ %small
+ #{number_with_delimiter(merge_request_diff.commits.count)} #{'commit'.pluralize(merge_request_diff.commits.count)},
+ = time_ago_with_tooltip(merge_request_diff.created_at)
+
+ - unless @merge_request_diff.latest?
+ %span.prepend-left-default
+ = icon('info-circle')
+ This version is not the latest one. Comments are disabled
+ .pull-right
+ %span.monospace
+ #{@merge_request_diff.base_commit.short_id}..#{@merge_request_diff.head_commit.short_id}
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index ea4898f2107..0a1e2bb2cc6 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -60,11 +60,6 @@
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
%div
- - if gitorious_import_enabled?
- = link_to new_import_gitorious_path, class: 'btn import_gitorious' do
- %i.icon-gitorious.icon-gitorious-small
- Gitorious.org
- %div
- if google_code_import_enabled?
= link_to new_import_google_code_path, class: 'btn import_google_code' do
= icon('google', text: 'Google Code')
diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index cf6e14648cc..6c14f48d41b 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -2,10 +2,10 @@
.comment-toolbar.clearfix
.toolbar-text
Styling with
- = link_to 'Markdown', help_page_path('markdown/markdown'), target: '_blank', tabindex: -1
+ = link_to 'Markdown', help_page_path('user/markdown'), target: '_blank', tabindex: -1
- if supports_slash_commands
and
- = link_to 'slash commands', help_page_path('workflow/slash_commands'), target: '_blank', tabindex: -1
+ = link_to 'slash commands', help_page_path('user/project/slash_commands'), target: '_blank', tabindex: -1
are
- else
is
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index d2ac1ce2b9a..7c82177f9ea 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -52,11 +52,11 @@
- if note.emoji_awardable?
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
- = icon('smile-o')
+ = icon('smile-o', class: 'link-highlight')
- if note_editable
= link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
- = icon('pencil')
+ = icon('pencil', class: 'link-highlight')
= link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button hidden-xs js-note-delete danger' do
= icon('trash-o')
.note-body{class: note_editable ? 'js-task-list-container' : ''}
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index d65faf86d4e..f611ddc8f5f 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,19 +1,21 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- - if project_nav_tab? :pipelines
- = nav_link(controller: :pipelines) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab? :pipelines
+ = nav_link(controller: :pipelines) do
+ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ %span
+ Pipelines
- - if project_nav_tab? :builds
- = nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
- %span
- Builds
+ - if project_nav_tab? :builds
+ = nav_link(controller: %w(builds)) do
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ %span
+ Builds
- - if project_nav_tab? :environments
- = nav_link(controller: %w(environments)) do
- = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
- %span
- Environments
+ - if project_nav_tab? :environments
+ = nav_link(controller: %w(environments)) do
+ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ %span
+ Environments
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 8289aefcde7..063e83a407a 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -9,7 +9,7 @@
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- if @pipeline.duration
in
- = time_interval_in_words @pipeline.duration
+ = time_interval_in_words(@pipeline.duration)
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index 5f466bdbac2..4d957e0d890 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -49,7 +49,10 @@
%th Commit
- stages.each do |stage|
%th.stage
- %span.has-tooltip{ title: "#{stage.titleize}" }
+ - if stage.titleize.length > 12
+ %span.has-tooltip{ title: "#{stage.titleize}" }
+ = stage.titleize
+ - else
= stage.titleize
%th
%th
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 978c4dfc5ec..fa8cbf71733 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this project.
+
.form-actions
= f.submit 'Add users to project', class: "btn btn-create"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9031f01b496..9d063b3081f 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Members"
-.project-members-page.prepend-top-default
+.project-members-page.js-project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 45f8ef89060..833954bc039 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index d4c6fa24768..e95a3b1b4c3 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -22,16 +22,20 @@
%label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
Allowed to merge:
.col-md-10
- = dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-merge wide',
- data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
+ .merge_access_levels-container
+ = dropdown_tag('Select',
+ options: { toggle_class: 'js-allowed-to-merge wide',
+ dropdown_class: 'dropdown-menu-selectable',
+ data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
.form-group
%label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
Allowed to push:
.col-md-10
- = dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-push wide',
- data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
+ .push_access_levels-container
+ = dropdown_tag('Select',
+ options: { toggle_class: 'js-allowed-to-push wide',
+ dropdown_class: 'dropdown-menu-selectable',
+ data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
.panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 835398b6f98..33d5cbff420 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -1,18 +1,20 @@
+- @no_container = true
- page_title "Edit", @tag.name, "Tags"
= render "projects/commits/head"
-.row-content-block
- .oneline
- .title
- Release notes for tag
- %strong #{@tag.name}
+%div{ class: container_class }
+ .sub-header-block.no-bottom-space
+ .oneline
+ .title
+ Release notes for tag
+ %strong #{@tag.name}
+
-.prepend-top-default
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
.error-alert
- .form-actions.prepend-top-default
+ .prepend-top-default
= f.submit 'Save changes', class: 'btn btn-save'
= link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
diff --git a/app/views/projects/repositories/_download_archive.html.haml b/app/views/projects/repositories/_download_archive.html.haml
deleted file mode 100644
index 24658319060..00000000000
--- a/app/views/projects/repositories/_download_archive.html.haml
+++ /dev/null
@@ -1,37 +0,0 @@
-- ref = ref || nil
-- btn_class = btn_class || ''
-- split_button = split_button || false
-- if split_button == true
- %span.btn-group{class: btn_class}
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn col-xs-10', rel: 'nofollow' do
- %i.fa.fa-download
- %span Download zip
- %a.col-xs-2.btn.dropdown-toggle{ 'data-toggle' => 'dropdown' }
- %span.caret
- %span.sr-only
- Select Archive Format
- %ul.col-xs-10.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
- %li
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download zip
- %li
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar.gz
- %li
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.bz2'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar.bz2
- %li
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar'), rel: 'nofollow' do
- %i.fa.fa-download
- %span Download tar
-- else
- %span.btn-group{class: btn_class}
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'zip'), class: 'btn', rel: 'nofollow' do
- %i.fa.fa-download
- %span zip
- = link_to archive_namespace_project_repository_path(@project.namespace, @project, ref: ref, format: 'tar.gz'), class: 'btn', rel: 'nofollow' do
- %i.fa.fa-download
- %span tar.gz
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index a666d07e9eb..9adce776c1c 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -64,13 +64,15 @@
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
+
%li.project-repo-buttons-right
.project-repo-buttons.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
+ = render "projects/buttons/koding"
.btn-group.project-repo-btn-group
- = render "projects/buttons/download"
+ = render 'projects/buttons/download', project: @project, ref: @ref
= render 'projects/buttons/dropdown'
= render 'shared/notifications/button', notification_setting: @notification_setting
@@ -86,4 +88,4 @@
Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"}
- = render default_project_view \ No newline at end of file
+ = render default_project_view
diff --git a/app/views/projects/tags/_download.html.haml b/app/views/projects/tags/_download.html.haml
deleted file mode 100644
index 8a11dbfa9f4..00000000000
--- a/app/views/projects/tags/_download.html.haml
+++ /dev/null
@@ -1,14 +0,0 @@
-%span.btn-group
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), class: 'btn btn-default', rel: 'nofollow' do
- %span Source code
- %a.btn.btn-default.dropdown-toggle{ 'data-toggle' => 'dropdown' }
- %span.caret
- %span.sr-only
- Select Archive Format
- %ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'zip'), rel: 'nofollow' do
- %span Download zip
- %li
- = link_to archive_namespace_project_repository_path(project.namespace, project, ref: ref, format: 'tar.gz'), rel: 'nofollow' do
- %span Download tar.gz
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 2c11c0e5b21..a156d98bab8 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -11,8 +11,7 @@
= strip_gpg_signature(tag.message)
.controls
- - if can?(current_user, :download_code, @project)
- = render 'projects/tags/download', ref: tag.name, project: @project
+ = render 'projects/buttons/download', project: @project, ref: tag.name
- if can?(current_user, :push_code, @project)
= link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 395d7af6cbb..4dd7439b2d0 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -12,8 +12,7 @@
= icon('files-o')
= link_to namespace_project_commits_path(@project.namespace, @project, @tag.name), class: 'btn has-tooltip', title: 'Browse commits' do
= icon('history')
- - if can? current_user, :download_code, @project
- = render 'projects/tags/download', ref: @tag.name, project: @project
+ = render 'projects/buttons/download', project: @project, ref: @tag.name
- if can?(current_user, :admin_project, @project)
.pull-right
= link_to namespace_project_tag_path(@project.namespace, @project, @tag.name), class: 'btn btn-remove remove-row grouped has-tooltip', title: "Delete tag", method: :delete, data: { confirm: "Deleting the '#{@tag.name}' tag cannot be undone. Are you sure?" } do
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index 558e6146ae9..ca5d2d7722a 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -5,16 +5,17 @@
%tr
%th Name
%th Last Update
- %th.hidden-xs
- .pull-left Last Commit
- .last-commit.hidden-sm.pull-left
- &nbsp;
+ %th.hidden-xs.last-commit
+ Last Commit
+ .last-commit-content.hidden-sm
%i.fa.fa-angle-right
&nbsp;
%small.light
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
&ndash;
- = truncate(@commit.title, length: 50)
+ = time_ago_with_tooltip(@commit.committed_date)
+ &ndash;
+ = @commit.full_title
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'pull-right'
- if @path.present?
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index bf5360b4dee..37d341212af 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -10,8 +10,7 @@
%div{ class: container_class }
.tree-controls
= render 'projects/find_file_link'
- - if can? current_user, :download_code, @project
- = render 'projects/repositories/download_archive', ref: @ref, btn_class: 'hidden-xs hidden-sm btn-grouped', split_button: true
+ = render 'projects/buttons/download', project: @project, ref: @ref
#tree-holder.tree-holder.clearfix
.nav-block
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index f8ea479e0b1..551a20c1044 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,13 +1,15 @@
-.nav-links.sub-nav
- %ul{ class: (container_class) }
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+.scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
+ = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
- = nav_link(path: 'wikis#pages') do
- = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
+ = nav_link(path: 'wikis#pages') do
+ = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
- = nav_link(path: 'wikis#git_access') do
- = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
- Git Access
+ = nav_link(path: 'wikis#git_access') do
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
+ Git Access
- = render 'projects/wikis/new'
+ = render 'projects/wikis/new'
diff --git a/app/views/shared/_logo.svg b/app/views/shared/_logo.svg
index b07f1c5603e..9b67422da2c 100644
--- a/app/views/shared/_logo.svg
+++ b/app/views/shared/_logo.svg
@@ -1,9 +1,9 @@
-<svg width="36" height="36" id="tanuki-logo">
- <path id="tanuki-right-ear" class="tanuki-shape" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
- <path id="tanuki-left-ear" class="tanuki-shape" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
- <path id="tanuki-nose" class="tanuki-shape" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
- <path id="tanuki-right-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
- <path id="tanuki-left-eye" class="tanuki-shape" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
- <path id="tanuki-right-cheek" class="tanuki-shape" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
- <path id="tanuki-left-cheek" class="tanuki-shape" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
+<svg width="36" height="36" class="tanuki-logo">
+ <path class="tanuki-shape tanuki-left-ear" fill="#e24329" d="M2 14l9.38 9v-9l-4-12.28c-.205-.632-1.176-.632-1.38 0z"/>
+ <path class="tanuki-shape tanuki-right-ear" fill="#e24329" d="M34 14l-9.38 9v-9l4-12.28c.205-.632 1.176-.632 1.38 0z"/>
+ <path class="tanuki-shape tanuki-nose" fill="#e24329" d="M18,34.38 3,14 33,14 Z"/>
+ <path class="tanuki-shape tanuki-left-eye" fill="#fc6d26" d="M18,34.38 11.38,14 2,14 6,25Z"/>
+ <path class="tanuki-shape tanuki-right-eye" fill="#fc6d26" d="M18,34.38 24.62,14 34,14 30,25Z"/>
+ <path class="tanuki-shape tanuki-left-cheek" fill="#fca326" d="M2 14L.1 20.16c-.18.565 0 1.2.5 1.56l17.42 12.66z"/>
+ <path class="tanuki-shape tanuki-right-cheek" fill="#fca326" d="M34 14l1.9 6.16c.18.565 0 1.2-.5 1.56L18 34.38z"/>
</svg>
diff --git a/app/views/shared/_nav_scroll.html.haml b/app/views/shared/_nav_scroll.html.haml
new file mode 100644
index 00000000000..4e3b1b3a571
--- /dev/null
+++ b/app/views/shared/_nav_scroll.html.haml
@@ -0,0 +1,4 @@
+.fade-left
+ = icon('angle-left')
+.fade-right
+ = icon('angle-right') \ No newline at end of file
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index ea7162d4d63..9a8252ab087 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -6,7 +6,7 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title "Switch branch/tag"
= dropdown_filter "Search branches and tags"
diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg
new file mode 100644
index 00000000000..80a6d41dbf6
--- /dev/null
+++ b/app/views/shared/icons/_icon_play.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_status_created.svg b/app/views/shared/icons/_icon_status_created.svg
new file mode 100644
index 00000000000..1f5c3b51b03
--- /dev/null
+++ b/app/views/shared/icons/_icon_status_created.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 14 14" enable-background="new 0 0 14 14"><path d="M12.5,7 C12.5,4 10,1.5 7,1.5 C4,1.5 1.5,4 1.5,7 C1.5,10 4,12.5 7,12.5 C10,12.5 12.5,10 12.5,7 L12.5,7 Z M0,7 C0,3.1 3.1,0 7,0 C10.9,0 14,3.1 14,7 C14,10.9 10.9,14 7,14 C3.1,14 0,10.9 0,7 L0,7 Z" /><circle cx="7" cy="7" r="3.25"/></svg>
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 4f8ea7e7cef..0f4f744a71f 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -27,15 +27,18 @@
= render "shared/issuable/label_dropdown"
.pull-right
- - if controller.controller_name == 'boards' && can?(current_user, :admin_list, @project)
- .dropdown
- %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
- Create new list
- .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
- = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
- - if can?(current_user, :admin_label, @project)
- = render partial: "shared/issuable/label_page_create"
- = dropdown_loading
+ - if controller.controller_name == 'boards'
+ #js-boards-seach.issue-boards-search
+ %input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
+ - if can?(current_user, :admin_list, @project)
+ .dropdown.pull-right
+ %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
+ Create new list
+ .dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
+ - if can?(current_user, :admin_label, @project)
+ = render partial: "shared/issuable/label_page_create"
+ = dropdown_loading
- else
= render 'shared/sort_dropdown'
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index d717c3d92ee..3856a4917b4 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,5 +1,12 @@
= form_errors(issuable)
+- if @conflict
+ .alert.alert-danger
+ Someone edited the #{issuable.class.model_name.human.downcase} the same time you did.
+ Please check out
+ = link_to "the #{issuable.class.model_name.human.downcase}", polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), target: "_blank"
+ and make sure your changes will not unintentionally remove theirs
+
.form-group
= f.label :title, class: 'control-label'
@@ -42,7 +49,7 @@
- if can_add_template?(issuable)
%p.help-block
Add
- = link_to "description templates", help_page_path('user/project/description_templates')
+ = link_to "description templates", help_page_path('user/project/description_templates'), tabindex: -1
to help your contributors communicate effectively!
.form-group.detail-page-description
@@ -121,13 +128,13 @@
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) }
+ = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
&nbsp;
%span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
= icon('question-circle')
-- if issuable.is_a?(MergeRequest)
+- if issuable.is_a?(MergeRequest) && !issuable.closed_without_fork?
%hr
- if @merge_request.new_record?
.form-group
@@ -168,7 +175,9 @@
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class]), class: 'btn btn-cancel'
- else
.pull-right
- - if current_user.can?(:"destroy_#{issuable.to_ability_name}", @project)
+ - if can?(current_user, :"destroy_#{issuable.to_ability_name}", @project)
= link_to 'Delete', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), data: { confirm: "#{issuable.class.name.titleize} will be removed! Are you sure?" },
method: :delete, class: 'btn btn-danger btn-grouped'
= link_to 'Cancel', polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable]), class: 'btn btn-grouped btn-cancel'
+
+= f.hidden_field :lock_version
diff --git a/app/views/shared/issuable/_label_page_default.html.haml b/app/views/shared/issuable/_label_page_default.html.haml
index a76b7baf918..c0dc63be2bf 100644
--- a/app/views/shared/issuable/_label_page_default.html.haml
+++ b/app/views/shared/issuable/_label_page_default.html.haml
@@ -8,7 +8,10 @@
- if show_boards_content
.issue-board-dropdown-content
%p
- Each label that exists in your issue tracker can have its own dedicated list. Select a label below to add a list to your Board and it will automatically be populated with issues that have that label. To create a list for a label that doesn't exist yet, simply create the label below.
+ Each label that exists in your issue tracker can have its own dedicated
+ list. Select a label below to add a list to your Board and it will
+ automatically be populated with issues that have that label. To create
+ a list for a label that doesn't exist yet, simply create the label below.
= dropdown_filter(filter_placeholder)
= dropdown_content
- if @project && show_footer
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 8e2fcbdfab8..c1b50e65af5 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -109,7 +109,7 @@
- if issuable.project.labels.any?
.block.labels
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
= icon('tags')
%span
= issuable.labels_array.size
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index fc6e206d082..5f20e4bd42a 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -16,7 +16,7 @@
= button_tag icon('pencil'),
type: 'button',
class: 'btn inline js-toggle-button',
- title: 'Edit access level'
+ title: 'Edit'
- if member.request?
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
@@ -59,6 +59,10 @@
= time_ago_with_tooltip(member.requested_at)
- else
Joined #{time_ago_with_tooltip(member.created_at)}
+ - if member.expires?
+ ·
+ %span{ class: ('text-warning' if member.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
= image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
@@ -73,8 +77,16 @@
- if show_roles
.edit-member.hide.js-toggle-content
%br
- = form_for member, remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
+ = form_for member, remote: true, html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label'
+ .col-sm-10
+ = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}"
+ .form-group
+ = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
+ %i.clear-icon.js-clear-input
.prepend-top-10
= f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 47ec09f62c6..0c788032020 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,3 +1,7 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('snippet/snippet_bundle.js')
+
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
= form_errors(@snippet)
@@ -31,8 +35,3 @@
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
-:javascript
- var editor = ace.edit("editor");
- $(".snippet-form-holder form").submit(function(){
- $(".snippet-file-content").val(editor.getValue());
- });
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index c6a5af2809a..1dc7e0adef7 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -33,13 +33,13 @@ class EmailsOnPushWorker
reverse_compare = false
if action == :push
- compare = CompareService.new.execute(project, before_sha, project, after_sha)
+ compare = CompareService.new.execute(project, after_sha, project, before_sha)
diff_refs = compare.diff_refs
return false if compare.same
if compare.commits.empty?
- compare = CompareService.new.execute(project, after_sha, project, before_sha)
+ compare = CompareService.new.execute(project, before_sha, project, after_sha)
diff_refs = compare.diff_refs
reverse_compare = true
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
new file mode 100644
index 00000000000..246c8b6650a
--- /dev/null
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -0,0 +1,7 @@
+class RemoveExpiredGroupLinksWorker
+ include Sidekiq::Worker
+
+ def perform
+ ProjectGroupLink.expired.destroy_all
+ end
+end
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
new file mode 100644
index 00000000000..cf765af97ce
--- /dev/null
+++ b/app/workers/remove_expired_members_worker.rb
@@ -0,0 +1,13 @@
+class RemoveExpiredMembersWorker
+ include Sidekiq::Worker
+
+ def perform
+ Member.expired.find_each do |member|
+ begin
+ Members::AuthorizedDestroyService.new(member).execute
+ rescue => ex
+ logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
+ end
+ end
+ end
+end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index e6701078f71..d2ca8813ab9 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -14,6 +14,8 @@ class RepositoryImportWorker
import_url: @project.import_url,
path: @project.path_with_namespace)
+ project.update_column(:import_error, nil)
+
result = Projects::ImportService.new(project, current_user).execute
if result[:status] == :error
diff --git a/config/application.rb b/config/application.rb
index 6b80f8ddafa..4792f6670a8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -88,6 +88,8 @@ module Gitlab
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
+ config.assets.precompile << "blob_edit/blob_edit_bundle.js"
+ config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index deac3b0f0f9..4a01b9e40fb 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -212,7 +212,7 @@ Settings.gitlab.default_projects_features['builds'] = true if Settin
Settings.gitlab.default_projects_features['container_registry'] = true if Settings.gitlab.default_projects_features['container_registry'].nil?
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['domain_whitelist'] ||= []
-Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project]
+Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab google_code fogbugz git gitlab_project]
Settings.gitlab['trusted_proxies'] ||= []
#
@@ -293,6 +293,12 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor
Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker'
+Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
+Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
#
# GitLab Shell
diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/ar_monkey_patch.rb
new file mode 100644
index 00000000000..0da584626ee
--- /dev/null
+++ b/config/initializers/ar_monkey_patch.rb
@@ -0,0 +1,57 @@
+# rubocop:disable Lint/RescueException
+
+# This patch fixes https://github.com/rails/rails/issues/26024
+# TODO: Remove it when it's no longer necessary
+
+module ActiveRecord
+ module Locking
+ module Optimistic
+ # We overwrite this method because we don't want to have default value
+ # for newly created records
+ def _create_record(attribute_names = self.attribute_names, *) # :nodoc:
+ super
+ end
+
+ def _update_record(attribute_names = self.attribute_names) #:nodoc:
+ return super unless locking_enabled?
+ return 0 if attribute_names.empty?
+
+ lock_col = self.class.locking_column
+
+ previous_lock_value = send(lock_col).to_i
+
+ # This line is added as a patch
+ previous_lock_value = nil if previous_lock_value == '0' || previous_lock_value == 0
+
+ increment_lock
+
+ attribute_names += [lock_col]
+ attribute_names.uniq!
+
+ begin
+ relation = self.class.unscoped
+
+ affected_rows = relation.where(
+ self.class.primary_key => id,
+ lock_col => previous_lock_value,
+ ).update_all(
+ attributes_for_update(attribute_names).map do |name|
+ [name, _read_attribute(name)]
+ end.to_h
+ )
+
+ unless affected_rows == 1
+ raise ActiveRecord::StaleObjectError.new(self, "update")
+ end
+
+ affected_rows
+
+ # If something went wrong, revert the version.
+ rescue Exception
+ send(lock_col + '=', previous_lock_value)
+ raise
+ end
+ end
+ end
+ end
+end
diff --git a/config/routes.rb b/config/routes.rb
index 66f77aee06a..262a174437a 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -91,6 +91,11 @@ Rails.application.routes.draw do
get 'help/*path' => 'help#show', as: :help_page
#
+ # Koding route
+ #
+ get 'koding' => 'koding#index'
+
+ #
# Global snippets
#
resources :snippets do
@@ -152,12 +157,6 @@ Rails.application.routes.draw do
get :jobs
end
- resource :gitorious, only: [:create, :new], controller: :gitorious do
- get :status
- get :callback
- get :jobs
- end
-
resource :google_code, only: [:create, :new], controller: :google_code do
get :status
post :callback
@@ -783,6 +782,14 @@ Rails.application.routes.draw do
resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
collection do
post :cancel_all
+
+ resources :artifacts, only: [] do
+ collection do
+ get :latest_succeeded,
+ path: '*ref_name_and_path',
+ format: false
+ end
+ end
end
member do
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_pipelines.rb
index 069d9dd6226..49e6e2361b1 100644
--- a/db/fixtures/development/14_builds.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -1,4 +1,4 @@
-class Gitlab::Seeder::Builds
+class Gitlab::Seeder::Pipelines
STAGES = %w[build test deploy notify]
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
@@ -7,11 +7,12 @@ class Gitlab::Seeder::Builds
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:osx', stage: 'test', status_event: :success },
- { name: 'spinach:linux', stage: 'test', status: :pending },
- { name: 'spinach:osx', stage: 'test', status: :canceled },
- { name: 'cucumber:linux', stage: 'test', status: :running },
- { name: 'cucumber:osx', stage: 'test', status: :failed },
- { name: 'staging', stage: 'deploy', environment: 'staging', status: :success },
+ { name: 'spinach:linux', stage: 'test', status: :success },
+ { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
+ { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
+ { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
+ { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
+ { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
]
@@ -34,72 +35,86 @@ class Gitlab::Seeder::Builds
end
end
+ private
+
def pipelines
- master_pipelines + merge_request_pipelines
+ create_master_pipelines + create_merge_request_pipelines
end
- def master_pipelines
- create_pipelines_for(@project, 'master')
+ def create_master_pipelines
+ @project.repository.commits('master', limit: 4).map do |commit|
+ create_pipeline!(@project, 'master', commit)
+ end
rescue
[]
end
- def merge_request_pipelines
- @project.merge_requests.last(5).map do |merge_request|
- create_pipelines(merge_request.source_project, merge_request.source_branch, merge_request.commits.last(5))
- end.flatten
+ def create_merge_request_pipelines
+ pipelines = @project.merge_requests.first(3).map do |merge_request|
+ project = merge_request.source_project
+ branch = merge_request.source_branch
+
+ merge_request.commits.last(4).map do |commit|
+ create_pipeline!(project, branch, commit)
+ end
+ end
+
+ pipelines.flatten
rescue
[]
end
- def create_pipelines_for(project, ref)
- commits = project.repository.commits(ref, limit: 5)
- create_pipelines(project, ref, commits)
+
+ def create_pipeline!(project, ref, commit)
+ project.pipelines.create(sha: commit.id, ref: ref)
end
- def create_pipelines(project, ref, commits)
- commits.map do |commit|
- project.pipelines.create(sha: commit.id, ref: ref)
+ def build_create!(pipeline, opts = {})
+ attributes = job_attributes(pipeline, opts)
+ .merge(commands: '$ build command')
+
+ Ci::Build.create!(attributes).tap do |build|
+ # We need to set build trace and artifacts after saving a build
+ # (id required), that is why we need `#tap` method instead of passing
+ # block directly to `Ci::Build#create!`.
+
+ setup_artifacts(build)
+ setup_build_log(build)
+ build.save
end
end
- def build_create!(pipeline, opts = {})
- attributes = build_attributes_for(pipeline, opts)
+ def setup_artifacts(build)
+ return unless %w[build test].include?(build.stage)
- Ci::Build.create!(attributes) do |build|
- if opts[:name].start_with?('build')
- artifacts_cache_file(artifacts_archive_path) do |file|
- build.artifacts_file = file
- end
+ artifacts_cache_file(artifacts_archive_path) do |file|
+ build.artifacts_file = file
+ end
- artifacts_cache_file(artifacts_metadata_path) do |file|
- build.artifacts_metadata = file
- end
- end
+ artifacts_cache_file(artifacts_metadata_path) do |file|
+ build.artifacts_metadata = file
+ end
+ end
- if %w(running success failed).include?(build.status)
- # We need to set build trace after saving a build (id required)
- build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
- end
+ def setup_build_log(build)
+ if %w(running success failed).include?(build.status)
+ build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
end
end
def commit_status_create!(pipeline, opts = {})
- attributes = commit_status_attributes_for(pipeline, opts)
+ attributes = job_attributes(pipeline, opts)
+
GenericCommitStatus.create!(attributes)
end
- def commit_status_attributes_for(pipeline, opts)
+ def job_attributes(pipeline, opts)
{ name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline,
created_at: Time.now, updated_at: Time.now
}.merge(opts)
end
- def build_attributes_for(pipeline, opts)
- commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command')
- end
-
def build_user
@project.team.users.sample
end
@@ -131,8 +146,8 @@ class Gitlab::Seeder::Builds
end
Gitlab::Seeder.quiet do
- Project.all.sample(10).each do |project|
- project_builds = Gitlab::Seeder::Builds.new(project)
+ Project.all.sample(5).each do |project|
+ project_builds = Gitlab::Seeder::Pipelines.new(project)
project_builds.seed!
end
end
diff --git a/db/migrate/20160707104333_add_lock_to_issuables.rb b/db/migrate/20160707104333_add_lock_to_issuables.rb
new file mode 100644
index 00000000000..54866d02cbc
--- /dev/null
+++ b/db/migrate/20160707104333_add_lock_to_issuables.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLockToIssuables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column :issues, :lock_version, :integer
+ add_column :merge_requests, :lock_version, :integer
+ end
+
+ def down
+ remove_column :issues, :lock_version
+ remove_column :merge_requests, :lock_version
+ end
+end
diff --git a/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
new file mode 100644
index 00000000000..c8cbd2718ff
--- /dev/null
+++ b/db/migrate/20160725104020_merge_request_diff_remove_uniq.rb
@@ -0,0 +1,21 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class MergeRequestDiffRemoveUniq < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_index :merge_request_diffs, :merge_request_id
+ end
+ end
+
+ def down
+ unless index_exists?(:merge_request_diffs, :merge_request_id)
+ add_concurrent_index :merge_request_diffs, :merge_request_id, unique: true
+ end
+ end
+end
diff --git a/db/migrate/20160725104452_merge_request_diff_add_index.rb b/db/migrate/20160725104452_merge_request_diff_add_index.rb
new file mode 100644
index 00000000000..6d04242dd25
--- /dev/null
+++ b/db/migrate/20160725104452_merge_request_diff_add_index.rb
@@ -0,0 +1,17 @@
+class MergeRequestDiffAddIndex < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def up
+ add_concurrent_index :merge_request_diffs, :merge_request_id
+ end
+
+ def down
+ if index_exists?(:merge_request_diffs, :merge_request_id)
+ remove_index :merge_request_diffs, :merge_request_id
+ end
+ end
+end
diff --git a/db/migrate/20160801163421_add_expires_at_to_member.rb b/db/migrate/20160801163421_add_expires_at_to_member.rb
new file mode 100644
index 00000000000..8db0fc60c4b
--- /dev/null
+++ b/db/migrate/20160801163421_add_expires_at_to_member.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddExpiresAtToMember < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :members, :expires_at, :date
+ end
+end
diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb
new file mode 100644
index 00000000000..915d3d78e40
--- /dev/null
+++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb
@@ -0,0 +1,10 @@
+class AddKodingToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :koding_enabled, :boolean
+ add_column :application_settings, :koding_url, :string
+ end
+end
diff --git a/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb
new file mode 100644
index 00000000000..0ed538b0df8
--- /dev/null
+++ b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddExpiresAtToProjectGroupLinks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :project_group_links, :expires_at, :date
+ end
+end
diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
new file mode 100644
index 00000000000..b6e8bb18e7b
--- /dev/null
+++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToNoteDiscussionId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :notes, :discussion_id
+ end
+end
diff --git a/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb
new file mode 100644
index 00000000000..0c68cf01900
--- /dev/null
+++ b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetDiffNoteDiscussionIdBecauseItWasCalculatedWrongly < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ execute "UPDATE notes SET discussion_id = NULL WHERE discussion_id IS NOT NULL AND type = 'DiffNote'"
+ end
+end
diff --git a/db/migrate/20160823081327_change_merge_error_to_text.rb b/db/migrate/20160823081327_change_merge_error_to_text.rb
new file mode 100644
index 00000000000..7920389cd83
--- /dev/null
+++ b/db/migrate/20160823081327_change_merge_error_to_text.rb
@@ -0,0 +1,10 @@
+class ChangeMergeErrorToText < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = 'This migration requires downtime because it alters a column from varchar(255) to text.'
+
+ def change
+ change_column :merge_requests, :merge_error, :text, limit: 65535
+ end
+end
diff --git a/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb b/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb
new file mode 100644
index 00000000000..c169084e976
--- /dev/null
+++ b/db/migrate/20160823213309_add_lfs_enabled_to_projects.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLfsEnabledToProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :projects, :lfs_enabled, :boolean
+ end
+end
diff --git a/db/migrate/20160824103857_drop_unused_ci_tables.rb b/db/migrate/20160824103857_drop_unused_ci_tables.rb
new file mode 100644
index 00000000000..65cf46308d9
--- /dev/null
+++ b/db/migrate/20160824103857_drop_unused_ci_tables.rb
@@ -0,0 +1,11 @@
+class DropUnusedCiTables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ drop_table(:ci_services)
+ drop_table(:ci_web_hooks)
+ end
+end
diff --git a/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb
new file mode 100644
index 00000000000..7c55bc23cf2
--- /dev/null
+++ b/db/migrate/20160827011312_ensure_lock_version_has_no_default.rb
@@ -0,0 +1,16 @@
+class EnsureLockVersionHasNoDefault < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_default :issues, :lock_version, nil
+ change_column_default :merge_requests, :lock_version, nil
+
+ execute('UPDATE issues SET lock_version = 1 WHERE lock_version = 0')
+ execute('UPDATE merge_requests SET lock_version = 1 WHERE lock_version = 0')
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20160830232601_change_lock_version_not_null.rb b/db/migrate/20160830232601_change_lock_version_not_null.rb
new file mode 100644
index 00000000000..01c58ed5bdc
--- /dev/null
+++ b/db/migrate/20160830232601_change_lock_version_not_null.rb
@@ -0,0 +1,13 @@
+class ChangeLockVersionNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ change_column_null :issues, :lock_version, true
+ change_column_null :merge_requests, :lock_version, true
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 82d4590f6b5..963d528d170 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: 20160817154936) do
+ActiveRecord::Schema.define(version: 20160830232601) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -84,12 +84,14 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "health_check_access_token"
t.boolean "send_user_confirmation_email", default: false
t.integer "container_registry_token_expire_delay", default: 5
- t.boolean "user_default_external", default: false, null: false
t.text "after_sign_up_text"
+ t.boolean "user_default_external", default: false, null: false
t.string "repository_storage", default: "default"
t.string "enabled_git_access_protocol"
t.boolean "domain_blacklist_enabled", default: false
t.text "domain_blacklist"
+ t.boolean "koding_enabled"
+ t.string "koding_url"
end
create_table "audit_events", force: :cascade do |t|
@@ -173,8 +175,8 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.text "artifacts_metadata"
t.integer "erased_by_id"
t.datetime "erased_at"
- t.string "environment"
t.datetime "artifacts_expire_at"
+ t.string "environment"
t.integer "artifacts_size"
t.string "when"
t.text "yaml_variables"
@@ -293,16 +295,6 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
- create_table "ci_services", force: :cascade 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
-
create_table "ci_sessions", force: :cascade do |t|
t.string "session_id", null: false
t.text "data"
@@ -358,13 +350,6 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_index "ci_variables", ["gl_project_id"], name: "index_ci_variables_on_gl_project_id", using: :btree
- create_table "ci_web_hooks", force: :cascade 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: :cascade do |t|
t.integer "deploy_key_id", null: false
t.integer "project_id", null: false
@@ -469,6 +454,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.datetime "deleted_at"
t.date "due_date"
t.integer "moved_to_id"
+ t.integer "lock_version"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -568,6 +554,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "invite_token"
t.datetime "invite_accepted_at"
t.datetime "requested_at"
+ t.date "expires_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
@@ -589,7 +576,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "start_commit_sha"
end
- add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", unique: true, using: :btree
+ add_index "merge_request_diffs", ["merge_request_id"], name: "index_merge_request_diffs_on_merge_request_id", using: :btree
create_table "merge_requests", force: :cascade do |t|
t.string "target_branch", null: false
@@ -609,13 +596,14 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.integer "position", default: 0
t.datetime "locked_at"
t.integer "updated_by_id"
- t.string "merge_error"
+ t.text "merge_error"
+ t.text "merge_params"
t.boolean "merge_when_build_succeeds", default: false, null: false
t.integer "merge_user_id"
t.string "merge_commit_sha"
t.datetime "deleted_at"
t.string "in_progress_merge_commit_sha"
- t.text "merge_params"
+ t.integer "lock_version"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -699,6 +687,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
+ add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
@@ -768,10 +757,10 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.integer "user_id", null: false
t.string "token", null: false
t.string "name", null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
t.boolean "revoked", default: false
t.datetime "expires_at"
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
add_index "personal_access_tokens", ["token"], name: "index_personal_access_tokens_on_token", unique: true, using: :btree
@@ -783,6 +772,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "group_access", default: 30, null: false
+ t.date "expires_at"
end
create_table "project_import_data", force: :cascade do |t|
@@ -833,8 +823,9 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.boolean "only_allow_merge_if_build_succeeds", default: false, null: false
t.boolean "has_external_issue_tracker"
t.string "repository_storage", default: "default", null: false
- t.boolean "has_external_wiki"
t.boolean "request_access_enabled", default: true, null: false
+ t.boolean "has_external_wiki"
+ t.boolean "lfs_enabled"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -955,8 +946,8 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "noteable_type"
t.string "title"
t.text "description"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
t.boolean "submitted_as_ham", default: false, null: false
end
@@ -1027,13 +1018,13 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_index "u2f_registrations", ["user_id"], name: "index_u2f_registrations_on_user_id", using: :btree
create_table "user_agent_details", force: :cascade do |t|
- t.string "user_agent", null: false
- t.string "ip_address", null: false
- t.integer "subject_id", null: false
- t.string "subject_type", null: false
+ t.string "user_agent", null: false
+ t.string "ip_address", null: false
+ t.integer "subject_id", null: false
+ t.string "subject_type", null: false
t.boolean "submitted", default: false, null: false
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
end
create_table "users", force: :cascade do |t|
diff --git a/doc/README.md b/doc/README.md
index fc51ea911b9..254394eb63e 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -2,6 +2,7 @@
## User documentation
+- [Account Security](user/account/security.md) Securing your account via two-factor authentication, etc.
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
@@ -29,6 +30,7 @@
- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
+- [Koding](administration/integration/koding.md) Set up Koding to use with GitLab.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
diff --git a/doc/administration/integration/koding.md b/doc/administration/integration/koding.md
new file mode 100644
index 00000000000..a2c358af095
--- /dev/null
+++ b/doc/administration/integration/koding.md
@@ -0,0 +1,242 @@
+# Koding & GitLab
+
+> [Introduced][ce-5909] in GitLab 8.11.
+
+This document will guide you through installing and configuring Koding with
+GitLab.
+
+First of all, to be able to use Koding and GitLab together you will need public
+access to your server. This allows you to use single sign-on from GitLab to
+Koding and using vms from cloud providers like AWS. Koding has a registry for
+VMs, called Kontrol and it runs on the same server as Koding itself, VMs from
+cloud providers register themselves to Kontrol via the agent that we put into
+provisioned VMs. This agent is called Klient and it provides Koding to access
+and manage the target machine.
+
+Kontrol and Klient are based on another technology called
+[Kite](https://github.com/koding/kite), that we have written at Koding. Which is a
+microservice framework that allows you to develop microservices easily.
+
+## Requirements
+
+### Hardware
+
+Minimum requirements are;
+
+ - 2 cores CPU
+ - 3G RAM
+ - 10G Storage
+
+If you plan to use AWS to install Koding it is recommended that you use at
+least a `c3.xlarge` instance.
+
+### Software
+
+ - [Git](https://git-scm.com)
+ - [Docker](https://www.docker.com)
+ - [docker-compose](https://www.docker.com/products/docker-compose)
+
+Koding can run on most of the UNIX based operating systems, since it's shipped
+as containerized with Docker support, it can work on any operating system that
+supports Docker.
+
+Required services are:
+
+- **PostgreSQL** - Kontrol and Service DB provider
+- **MongoDB** - Main DB provider the application
+- **Redis** - In memory DB used by both application and services
+- **RabbitMQ** - Message Queue for both application and services
+
+which are also provided as a Docker container by Koding.
+
+
+## Getting Started with Development Versions
+
+
+### Koding
+
+You can run `docker-compose` environment for developing koding by
+executing commands in the following snippet.
+
+```bash
+git clone https://github.com/koding/koding.git
+cd koding
+docker-compose up
+```
+
+This should start koding on `localhost:8090`.
+
+By default there is no team exists in Koding DB. You'll need to create a team
+called `gitlab` which is the default team name for GitLab integration in the
+configuration. To make things in order it's recommended to create the `gitlab`
+team first thing after setting up Koding.
+
+
+### GitLab
+
+To install GitLab to your environment for development purposes it's recommended
+to use GitLab Development Kit which you can get it from
+[here](https://gitlab.com/gitlab-org/gitlab-development-kit).
+
+After all those steps, gitlab should be running on `localhost:3000`
+
+
+## Integration
+
+Integration includes following components;
+
+ - Single Sign On with OAuth from GitLab to Koding
+ - System Hook integration for handling GitLab events on Koding
+ (`project_created`, `user_joined` etc.)
+ - Service endpoints for importing/executing stacks from GitLab to Koding
+ (`Run/Try on IDE (Koding)` buttons on GitLab Projects, Issues, MRs)
+
+As it's pointed out before, you will need public access to this machine that
+you've installed Koding and GitLab on. Better to use a domain but a static IP
+is also fine.
+
+For IP based installation you can use [xip.io](https://xip.io) service which is
+free and provides DNS resolution to IP based requests like following;
+
+ - 127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - and so on...
+
+As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for
+a running koding instance on `127.0.0.1` server will be handled as `foo` team
+requests.
+
+
+### GitLab Side
+
+You need to enable Koding integration from Settings under Admin Area. To do
+that login with an Admin account and do followings;
+
+ - open [http://127.0.0.1:3000/admin/application_settings](http://127.0.0.1:3000/admin/application_settings)
+ - scroll to bottom of the page until Koding section
+ - check `Enable Koding` checkbox
+ - provide GitLab team page for running Koding instance as `Koding URL`*
+
+* For `Koding URL` you need to provide the gitlab integration enabled team on
+your Koding installation. Team called `gitlab` has integration on Koding out
+of the box, so if you didn't change anything your team on Koding should be
+`gitlab`.
+
+So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs
+to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host
+with your Koding installation here.
+
+
+#### Registering Koding for OAuth integration
+
+We need `Application ID` and `Secret` to enable login to Koding via GitLab
+feature and to do that you need to register running Koding as a new application
+to your running GitLab application. Follow
+[these](http://docs.gitlab.com/ce/integration/oauth_provider.html) steps to
+enable this integration.
+
+Redirect URI should be `http://gitlab.127.0.0.1:8090/-/oauth/gitlab/callback`
+which again you need to _replace `127.0.0.1` with your instance public IP._
+
+Take a copy of `Application ID` and `Secret` that is generated by the GitLab
+application, we will need those on _Koding Part_ of this guide.
+
+
+#### Registering system hooks to Koding (optional)
+
+Koding can take actions based on the events generated by GitLab application.
+This feature is still in progress and only following events are processed by
+Koding at the moment;
+
+ - user_create
+ - user_destroy
+
+All system events are handled but not implemented on Koding side.
+
+To enable this feature you need to provide a `URL` and a `Secret Token` to your
+GitLab application. Open your admin area on your GitLab app from
+[http://127.0.0.1:3000/admin/hooks](http://127.0.0.1:3000/admin/hooks)
+and provide `URL` as `http://gitlab.127.0.0.1:8090/-/api/gitlab` which is the
+endpoint to handle GitLab events on Koding side. Provide a `Secret Token` and
+keep a copy of it, we will need it on _Koding Part_ of this guide.
+
+_(replace `127.0.0.1` with your instance public IP)_
+
+
+### Koding Part
+
+If you followed the steps in GitLab part we should have followings to enable
+Koding part integrations;
+
+ - `Application ID` and `Secret` for OAuth integration
+ - `Secret Token` for system hook integration
+ - Public address of running GitLab instance
+
+
+#### Start Koding with GitLab URL
+
+Now we need to configure Koding with all this information to get things ready.
+If it's already running please stop koding first.
+
+##### From command-line
+
+Replace followings with the ones you got from GitLab part of this guide;
+
+```bash
+cd koding
+docker-compose run \
+ --service-ports backend \
+ /opt/koding/scripts/bootstrap-container build \
+ --host=**YOUR_IP**.xip.io \
+ --gitlabHost=**GITLAB_IP** \
+ --gitlabPort=**GITLAB_PORT** \
+ --gitlabToken=**SECRET_TOKEN** \
+ --gitlabAppId=**APPLICATION_ID** \
+ --gitlabAppSecret=**SECRET**
+```
+
+##### By updating configuration
+
+Alternatively you can update `gitlab` section on
+`config/credentials.default.coffee` like following;
+
+```
+gitlab =
+ host: '**GITLAB_IP**'
+ port: '**GITLAB_PORT**'
+ applicationId: '**APPLICATION_ID**'
+ applicationSecret: '**SECRET**'
+ team: 'gitlab'
+ redirectUri: ''
+ systemHookToken: '**SECRET_TOKEN**'
+ hooksEnabled: yes
+```
+
+and start by only providing the `host`;
+
+```bash
+cd koding
+docker-compose run \
+ --service-ports backend \
+ /opt/koding/scripts/bootstrap-container build \
+ --host=**YOUR_IP**.xip.io \
+```
+
+#### Enable Single Sign On
+
+Once you restarted your Koding and logged in with your username and password
+you need to activate oauth authentication for your user. To do that
+
+ - Navigate to Dashboard on Koding from;
+ `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account`
+ - Scroll down to Integrations section
+ - Click on toggle to turn On integration in GitLab integration section
+
+This will redirect you to your GitLab instance and will ask your permission (
+if you are not logged in to GitLab at this point you will be redirected after
+login) once you accept you will be redirected to your Koding instance.
+
+From now on you can login by using `SIGN IN WITH GITLAB` button on your Login
+screen in your Koding instance.
+
+[ce-5909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5909
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 5c98c5d7565..55d0de7afd9 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -10,7 +10,7 @@ GET /projects/:id/repository/commits
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
| `since` | string | no | Only commits after or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `until` | string | no | Only commits before or in this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
@@ -58,7 +58,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -102,7 +102,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -138,7 +138,7 @@ Parameters:
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit hash or name of a repository branch or tag |
```bash
@@ -187,7 +187,7 @@ POST /projects/:id/repository/commits/:sha/comments
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project |
+| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA or name of a repository branch or tag |
| `note` | string | yes | The text of the comment |
| `path` | string | no | The file path relative to the repository |
@@ -232,7 +232,7 @@ GET /projects/:id/repository/commits/:sha/statuses
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project
+| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA
| `ref_name`| string | no | The name of a repository branch or tag or, if not given, the default branch
| `stage` | string | no | Filter by [build stage](../ci/yaml/README.md#stages), e.g., `test`
@@ -306,7 +306,7 @@ POST /projects/:id/statuses/:sha
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of a project
+| `id` | integer | yes | The ID of a project or NAMESPACE/PROJECT_NAME owned by the authenticated user
| `sha` | string | yes | The commit SHA
| `state` | string | yes | The state of the status. Can be one of the following: `pending`, `running`, `success`, `failed`, `canceled`
| `ref` | string | no | The `ref` (branch or tag) to which the status refers
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a665645ad0e..b194799ccbf 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -79,7 +79,8 @@ Example response:
"labels" : [],
"subscribed" : false,
"user_notes_count": 1,
- "due_date": "2016-07-22"
+ "due_date": "2016-07-22",
+ "web_url": "http://example.com/example/example/issues/6"
}
]
```
@@ -156,7 +157,8 @@ Example response:
"created_at" : "2016-01-04T15:31:46.176Z",
"subscribed" : false,
"user_notes_count": 1,
- "due_date": null
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/1"
}
]
```
@@ -235,7 +237,8 @@ Example response:
"created_at" : "2016-01-04T15:31:46.176Z",
"subscribed" : false,
"user_notes_count": 1,
- "due_date": "2016-07-22"
+ "due_date": "2016-07-22",
+ "web_url": "http://example.com/example/example/issues/1"
}
]
```
@@ -299,7 +302,8 @@ Example response:
"created_at" : "2016-01-04T15:31:46.176Z",
"subscribed": false,
"user_notes_count": 1,
- "due_date": null
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/1"
}
```
@@ -323,8 +327,8 @@ POST /projects/:id/issues
| `assignee_id` | integer | no | The ID of a user to assign issue |
| `milestone_id` | integer | no | The ID of a milestone to assign issue |
| `labels` | string | no | Comma-separated label names for an issue |
-| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` |
-| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `created_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
+| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues?title=Issues%20with%20auth&labels=bug
@@ -357,7 +361,8 @@ Example response:
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
- "due_date": null
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/14"
}
```
@@ -384,8 +389,8 @@ PUT /projects/:id/issues/:issue_id
| `milestone_id` | integer | no | The ID of a milestone to assign the issue to |
| `labels` | string | no | Comma-separated label names for an issue |
| `state_event` | string | no | The state event of an issue. Set `close` to close the issue and `reopen` to reopen it |
-| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` |
-| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
+| `updated_at` | string | no | Date time string, ISO 8601 formatted, e.g. `2016-03-11T03:45:40Z` (requires admin or project owner rights) |
+| `due_date` | string | no | Date time string in the format YEAR-MONTH-DAY, e.g. `2016-03-11` |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/4/issues/85?state_event=close
@@ -418,7 +423,8 @@ Example response:
"milestone" : null,
"subscribed" : true,
"user_notes_count": 0,
- "due_date": "2016-07-22"
+ "due_date": "2016-07-22",
+ "web_url": "http://example.com/example/example/issues/15"
}
```
@@ -496,7 +502,8 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin"
},
- "due_date": null
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/11"
}
```
@@ -551,7 +558,8 @@ Example response:
"avatar_url": "http://www.gravatar.com/avatar/7a190fecbaa68212a4b68aeb6e3acd10?s=80&d=identicon",
"web_url": "https://gitlab.example.com/u/solon.cremin"
},
- "due_date": null
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/11"
}
```
@@ -607,7 +615,8 @@ Example response:
"web_url": "https://gitlab.example.com/u/orville"
},
"subscribed": false,
- "due_date": null
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/12"
}
```
@@ -693,7 +702,9 @@ Example response:
"subscribed": true,
"user_notes_count": 7,
"upvotes": 0,
- "downvotes": 0
+ "downvotes": 0,
+ "due_date": null,
+ "web_url": "http://example.com/example/example/issues/110"
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/issues/10",
"body": "Vel voluptas atque dicta mollitia adipisci qui at.",
diff --git a/doc/api/members.md b/doc/api/members.md
index d002e6eaf89..fd6d728dad2 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -86,7 +86,8 @@ Example response:
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
- "access_level": 30
+ "access_level": 30,
+ "expires_at": null
}
```
@@ -106,6 +107,7 @@ POST /projects/:id/members
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the new member |
| `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
@@ -141,6 +143,7 @@ PUT /projects/:id/members/:user_id
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the member |
| `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 3e88a758936..f4760ceac7c 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -70,7 +70,8 @@ Parameters:
"subscribed" : false,
"user_notes_count": 1,
"should_remove_source_branch": true,
- "force_remove_source_branch": false
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
]
```
@@ -136,7 +137,8 @@ Parameters:
"subscribed" : true,
"user_notes_count": 1,
"should_remove_source_branch": true,
- "force_remove_source_branch": false
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -239,6 +241,7 @@ Parameters:
"user_notes_count": 1,
"should_remove_source_branch": true,
"force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1",
"changes": [
{
"old_path": "VERSION",
@@ -321,7 +324,8 @@ Parameters:
"subscribed" : true,
"user_notes_count": 0,
"should_remove_source_branch": true,
- "force_remove_source_branch": false
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -395,7 +399,8 @@ Parameters:
"subscribed" : true,
"user_notes_count": 1,
"should_remove_source_branch": true,
- "force_remove_source_branch": false
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -496,7 +501,8 @@ Parameters:
"subscribed" : true,
"user_notes_count": 1,
"should_remove_source_branch": true,
- "force_remove_source_branch": false
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -565,7 +571,8 @@ Parameters:
"subscribed" : true,
"user_notes_count": 1,
"should_remove_source_branch": true,
- "force_remove_source_branch": false
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
}
```
@@ -886,7 +893,8 @@ Example response:
"subscribed": true,
"user_notes_count": 7,
"should_remove_source_branch": true,
- "force_remove_source_branch": false
+ "force_remove_source_branch": false,
+ "web_url": "http://example.com/example/example/merge_requests/1"
},
"target_url": "https://gitlab.example.com/gitlab-org/gitlab-ci/merge_requests/7",
"body": "Et voluptas laudantium minus nihil recusandae ut accusamus earum aut non.",
@@ -894,3 +902,112 @@ Example response:
"created_at": "2016-07-01T11:14:15.530Z"
}
```
+
+## Get MR diff versions
+
+Get a list of merge request diff versions.
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/versions
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | String | yes | The ID of the project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions
+```
+
+Example response:
+
+```json
+[{
+ "id": 110,
+ "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-26T14:44:48.926Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1"
+}, {
+ "id": 108,
+ "head_commit_sha": "3eed087b29835c48015768f839d76e5ea8f07a24",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-25T14:21:33.028Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1"
+}]
+```
+
+## Get a single MR diff version
+
+Get a single merge request diff version.
+
+```
+GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `id` | String | yes | The ID of the project |
+| `merge_request_id` | integer | yes | The ID of the merge request |
+| `version_id` | integer | yes | The ID of the merge request diff version |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/1/merge_requests/1/versions/1
+```
+
+Example response:
+
+```json
+{
+ "id": 110,
+ "head_commit_sha": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "base_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "start_commit_sha": "eeb57dffe83deb686a60a71c16c32f71046868fd",
+ "created_at": "2016-07-26T14:44:48.926Z",
+ "merge_request_id": 105,
+ "state": "collected",
+ "real_size": "1",
+ "commits": [{
+ "id": "33e2ee8579fda5bc36accc9c6fbd0b4fefda9e30",
+ "short_id": "33e2ee85",
+ "title": "Change year to 2018",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-26T17:44:29.000+03:00",
+ "message": "Change year to 2018"
+ }, {
+ "id": "aa24655de48b36335556ac8a3cd8bb521f977cbd",
+ "short_id": "aa24655d",
+ "title": "Update LICENSE",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-25T17:21:53.000+03:00",
+ "message": "Update LICENSE"
+ }, {
+ "id": "3eed087b29835c48015768f839d76e5ea8f07a24",
+ "short_id": "3eed087b",
+ "title": "Add license",
+ "author_name": "Administrator",
+ "author_email": "admin@example.com",
+ "created_at": "2016-07-25T17:21:20.000+03:00",
+ "message": "Add license"
+ }],
+ "diffs": [{
+ "old_path": "LICENSE",
+ "new_path": "LICENSE",
+ "a_mode": "0",
+ "b_mode": "100644",
+ "diff": "--- /dev/null\n+++ b/LICENSE\n@@ -0,0 +1,21 @@\n+The MIT License (MIT)\n+\n+Copyright (c) 2018 Administrator\n+\n+Permission is hereby granted, free of charge, to any person obtaining a copy\n+of this software and associated documentation files (the \"Software\"), to deal\n+in the Software without restriction, including without limitation the rights\n+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell\n+copies of the Software, and to permit persons to whom the Software is\n+furnished to do so, subject to the following conditions:\n+\n+The above copyright notice and this permission notice shall be included in all\n+copies or substantial portions of the Software.\n+\n+THE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\n+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\n+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\n+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\n+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\n+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\n+SOFTWARE.\n",
+ "new_file": true,
+ "renamed_file": false,
+ "deleted_file": false
+ }]
+}
+```
diff --git a/doc/api/project_snippets.md b/doc/api/project_snippets.md
index a7acf37b5bc..c6685f54a9d 100644
--- a/doc/api/project_snippets.md
+++ b/doc/api/project_snippets.md
@@ -53,7 +53,8 @@ Parameters:
},
"expires_at": null,
"updated_at": "2012-06-28T10:52:04Z",
- "created_at": "2012-06-28T10:52:04Z"
+ "created_at": "2012-06-28T10:52:04Z",
+ "web_url": "http://example.com/example/example/snippets/1"
}
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 37d97b2db44..0d5aa61aa74 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -84,7 +84,8 @@ Parameters:
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
},
{
"id": 6,
@@ -144,7 +145,8 @@ Parameters:
"star_count": 0,
"runners_token": "b8547b1dc37721d05889db52fa2f02",
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
]
```
@@ -280,7 +282,8 @@ Parameters:
"group_name": "Gitlab Org",
"group_access_level": 10
}
- ]
+ ],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -448,6 +451,8 @@ Parameters:
- `visibility_level` (optional)
- `import_url` (optional)
- `public_builds` (optional)
+- `only_allow_merge_if_build_succeeds` (optional)
+- `lfs_enabled` (optional)
### Create project for user
@@ -473,6 +478,8 @@ Parameters:
- `visibility_level` (optional)
- `import_url` (optional)
- `public_builds` (optional)
+- `only_allow_merge_if_build_succeeds` (optional)
+- `lfs_enabled` (optional)
### Edit project
@@ -484,7 +491,7 @@ PUT /projects/:id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `name` (optional) - project name
- `path` (optional) - repository name for project
- `description` (optional) - short project description
@@ -499,6 +506,8 @@ Parameters:
- `public` (optional) - if `true` same as setting visibility_level = 20
- `visibility_level` (optional)
- `public_builds` (optional)
+- `only_allow_merge_if_build_succeeds` (optional)
+- `lfs_enabled` (optional)
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
@@ -513,7 +522,7 @@ POST /projects/fork/:id
Parameters:
-- `id` (required) - The ID of the project to be forked
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
### Star a project
@@ -526,7 +535,7 @@ POST /projects/:id/star
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -577,7 +586,8 @@ Example response:
"forks_count": 0,
"star_count": 1,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -592,7 +602,7 @@ DELETE /projects/:id/star
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -643,7 +653,8 @@ Example response:
"forks_count": 0,
"star_count": 0,
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -662,7 +673,7 @@ POST /projects/:id/archive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
@@ -729,7 +740,8 @@ Example response:
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -748,7 +760,7 @@ POST /projects/:id/unarchive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer | yes | The ID of the project |
+| `id` | integer | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
@@ -815,7 +827,8 @@ Example response:
"star_count": 0,
"runners_token": "b8bc4a7a29eb76ea83cf79e4908c2b",
"public_builds": true,
- "shared_with_groups": []
+ "shared_with_groups": [],
+ "only_allow_merge_if_build_succeeds": false
}
```
@@ -829,7 +842,7 @@ DELETE /projects/:id
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
## Uploads
@@ -843,7 +856,7 @@ POST /projects/:id/uploads
Parameters:
-- `id` (required) - The ID of the project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
- `file` (required) - The file to be uploaded
```json
@@ -872,7 +885,7 @@ POST /projects/:id/share
Parameters:
-- `id` (required) - The ID of a project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
- `group_id` (required) - The ID of a group
- `group_access` (required) - Level of permissions for sharing
@@ -914,7 +927,11 @@ Parameters:
"push_events": true,
"issues_events": true,
"merge_requests_events": true,
+ "tag_push_events": true,
"note_events": true,
+ "build_events": true,
+ "pipeline_events": true,
+ "wiki_page_events": true,
"enable_ssl_verification": true,
"created_at": "2012-10-12T17:04:47Z"
}
@@ -937,6 +954,9 @@ Parameters:
- `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
+- `build_events` - Trigger hook on build events
+- `pipeline_events` - Trigger hook on pipeline events
+- `wiki_page_events` - Trigger hook on wiki page events
- `enable_ssl_verification` - Do SSL verification when triggering the hook
### Edit project hook
@@ -957,6 +977,9 @@ Parameters:
- `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
+- `build_events` - Trigger hook on build events
+- `pipeline_events` - Trigger hook on pipeline events
+- `wiki_page_events` - Trigger hook on wiki page events
- `enable_ssl_verification` - Do SSL verification when triggering the hook
### Delete project hook
@@ -978,6 +1001,8 @@ is available before it is returned in the JSON response or an empty response is
## Branches
+For more information please consult the [Branches](branches.md) documentation.
+
### List branches
Lists all branches of a project.
@@ -996,56 +1021,46 @@ Parameters:
"name": "async",
"commit": {
"id": "a2b702edecdf41f07b42653eb1abe30ce98b9fca",
- "parents": [
- {
- "id": "3f94fc7c85061973edc9906ae170cc269b07ca55"
- }
+ "parent_ids": [
+ "3f94fc7c85061973edc9906ae170cc269b07ca55"
],
- "tree": "c68537c6534a02cc2b176ca1549f4ffa190b58ee",
"message": "give Caolan credit where it's due (up top)",
- "author": {
- "name": "Jeremy Ashkenas",
- "email": "jashkenas@example.com"
- },
- "committer": {
- "name": "Jeremy Ashkenas",
- "email": "jashkenas@example.com"
- },
+ "author_name": "Jeremy Ashkenas",
+ "author_email": "jashkenas@example.com",
"authored_date": "2010-12-08T21:28:50+00:00",
+ "committer_name": "Jeremy Ashkenas",
+ "committer_email": "jashkenas@example.com",
"committed_date": "2010-12-08T21:28:50+00:00"
},
- "protected": false
+ "protected": false,
+ "developers_can_push": false,
+ "developers_can_merge": false
},
{
"name": "gh-pages",
"commit": {
"id": "101c10a60019fe870d21868835f65c25d64968fc",
- "parents": [
- {
- "id": "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
- }
+ "parent_ids": [
+ "9c15d2e26945a665131af5d7b6d30a06ba338aaa"
],
- "tree": "fb5cc9d45da3014b17a876ad539976a0fb9b352a",
"message": "Underscore.js 1.5.2",
- "author": {
- "name": "Jeremy Ashkenas",
- "email": "jashkenas@example.com"
- },
- "committer": {
- "name": "Jeremy Ashkenas",
- "email": "jashkenas@example.com"
- },
+ "author_name": "Jeremy Ashkenas",
+ "author_email": "jashkenas@example.com",
"authored_date": "2013-09-07T12:58:21+00:00",
+ "committer_name": "Jeremy Ashkenas",
+ "committer_email": "jashkenas@example.com",
"committed_date": "2013-09-07T12:58:21+00:00"
},
- "protected": false
+ "protected": false,
+ "developers_can_push": false,
+ "developers_can_merge": false
}
]
```
-### List single branch
+### Single branch
-Lists a specific branch of a project.
+A specific branch of a project.
```
GET /projects/:id/repository/branches/:branch
@@ -1055,6 +1070,8 @@ Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
- `branch` (required) - The name of the branch.
+- `developers_can_push` - Flag if developers can push to the branch.
+- `developers_can_merge` - Flag if developers can merge to the branch.
### Protect single branch
@@ -1094,7 +1111,7 @@ POST /projects/:id/fork/:forked_from_id
Parameters:
-- `id` (required) - The ID of the project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
- `forked_from_id:` (required) - The ID of the project that was forked from
### Delete an existing forked from relationship
@@ -1105,7 +1122,7 @@ DELETE /projects/:id/fork
Parameter:
-- `id` (required) - The ID of the project
+- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
## Search for projects by name
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index d90d7aca4fd..ca9b986a060 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -5,7 +5,7 @@ Introduced in GitLab 8.8.
## Pipelines
-A pipeline is a group of [builds] that get executed in [stages] (batches). All
+A pipeline is a group of [builds] that get executed in [stages] \(batches). All
of the builds in a stage are executed in parallel (if there are enough
concurrent [runners]), and if they all succeed, the pipeline moves on to the
next stage. If one of the builds fails, the next stage is not (usually)
@@ -67,6 +67,8 @@ use following Markdown code to embed the est coverage report into `README.md`:
![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
```
+The latest successful pipeline will be used to read the test coverage value.
+
[builds]: #builds
[jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index e7850aa2c9d..58d5306f12a 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -39,7 +39,7 @@ If you want a quick introduction to GitLab CI, follow our
- [before_script and after_script](#before_script-and-after_script)
- [Git Strategy](#git-strategy)
- [Shallow cloning](#shallow-cloning)
-- [Hidden jobs](#hidden-jobs)
+- [Hidden keys](#hidden-keys)
- [Special YAML features](#special-yaml-features)
- [Anchors](#anchors)
- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
@@ -934,24 +934,27 @@ variables:
GIT_DEPTH: "3"
```
-## Hidden jobs
+## Hidden keys
>**Note:**
Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
-Jobs that start with a dot (`.`) will be not processed by GitLab CI. You can
+Keys that start with a dot (`.`) will be not processed by GitLab CI. You can
use this feature to ignore jobs, or use the
-[special YAML features](#special-yaml-features) and transform the hidden jobs
+[special YAML features](#special-yaml-features) and transform the hidden keys
into templates.
-In the following example, `.job_name` will be ignored:
+In the following example, `.key_name` will be ignored:
```yaml
-.job_name:
+.key_name:
script:
- rake spec
```
+Hidden keys can be hashes like normal CI jobs, but you are also allowed to use
+different types of structures to leverage special YAML features.
+
## Special YAML features
It's possible to use special YAML features like anchors (`&`), aliases (`*`)
@@ -967,7 +970,7 @@ Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
YAML also has a handy feature called 'anchors', which let you easily duplicate
content across your document. Anchors can be used to duplicate/inherit
-properties, and is a perfect example to be used with [hidden jobs](#hidden-jobs)
+properties, and is a perfect example to be used with [hidden keys](#hidden-keys)
to provide templates for your jobs.
The following example uses anchors and map merging. It will create two jobs,
@@ -975,7 +978,7 @@ The following example uses anchors and map merging. It will create two jobs,
having their own custom `script` defined:
```yaml
-.job_template: &job_definition # Hidden job that defines an anchor named 'job_definition'
+.job_template: &job_definition # Hidden key that defines an anchor named 'job_definition'
image: ruby:2.1
services:
- postgres
@@ -1081,7 +1084,7 @@ test:mysql:
- ruby
```
-You can see that the hidden jobs are conveniently used as templates.
+You can see that the hidden keys are conveniently used as templates.
## Validate the .gitlab-ci.yml
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index 927a1872413..37bb59e112c 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -6,7 +6,7 @@ it organized and easy to find.
## Location and naming of documents
>**Note:**
-These guidelines derive from the discussion taken place in issue [#3349](ce-3349).
+These guidelines derive from the discussion taken place in issue [#3349][ce-3349].
The documentation hierarchy can be vastly improved by providing a better layout
and organization of directories.
@@ -222,18 +222,26 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
```
1. Find and replace any occurrences of the old location with the new one.
- A quick way to find them is to use `grep`:
+ A quick way to find them is to use `git grep`. First go to the root directory
+ where you cloned the `gitlab-ce` repository and then do:
```
- grep -nR "lfs_administration.md" doc/
+ git grep -n "workflow/lfs/lfs_administration"
+ git grep -n "lfs/lfs_administration"
```
- The above command will search in the `doc/` directory for
- `lfs_administration.md` recursively and will print the file and the line
- where this file is mentioned. Note that we used just the filename
- (`lfs_administration.md`) and not the whole the relative path
- (`workflow/lfs/lfs_administration.md`).
+Things to note:
+- Since we also use inline documentation, except for the documentation itself,
+ the document might also be referenced in the views of GitLab (`app/`) which will
+ render when visiting `/help`, and sometimes in the testing suite (`spec/`).
+- The above `git grep` command will search recursively in the directory you run
+ it in for `workflow/lfs/lfs_administration` and `lfs/lfs_administration`
+ and will print the file and the line where this file is mentioned.
+ You may ask why the two greps. Since we use relative paths to link to
+ documentation, sometimes it might be useful to search a path deeper.
+- The `*.md` extension is not used when a document is linked to GitLab's
+ built-in help page, that's why we omit it in `git grep`.
## Configuration documentation for source and Omnibus installations
@@ -422,7 +430,7 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain
[cURL]: http://curl.haxx.se/ "cURL website"
[single spaces]: http://www.slate.com/articles/technology/technology/2011/01/space_invaders.html
-[gfm]: http://docs.gitlab.com/ce/markdown/markdown.html#newlines "GitLab flavored markdown documentation"
+[gfm]: http://docs.gitlab.com/ce/user/markdown.html#newlines "GitLab flavored markdown documentation"
[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
[ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
diff --git a/doc/development/newlines_styleguide.md b/doc/development/newlines_styleguide.md
index e03adcaadea..32aac2529a4 100644
--- a/doc/development/newlines_styleguide.md
+++ b/doc/development/newlines_styleguide.md
@@ -2,7 +2,7 @@
This style guide recommends best practices for newlines in Ruby code.
-## Rule: separate code with newlines only when it makes sense from logic perspectice
+## Rule: separate code with newlines only to group together related logic
```ruby
# bad
diff --git a/doc/install/installation.md b/doc/install/installation.md
index eb9606934cd..d4b89fa8345 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -397,7 +397,7 @@ If you are not using Linux you may have to run `gmake` instead of
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
- sudo -u git -H git checkout v0.7.8
+ sudo -u git -H git checkout v0.7.11
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index a65ac8a5f79..571f1a38358 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -32,7 +32,7 @@ Please consider using a virtual machine to run GitLab.
## Ruby versions
-GitLab requires Ruby (MRI) 2.1.x and currently does not work with versions 2.2 or 2.3.
+GitLab requires Ruby (MRI) 2.3. Support for Ruby versions below 2.3 (2.1, 2.2) will stop with GitLab 8.13.
You will have to use the standard MRI implementation of Ruby.
We love [JRuby](http://jruby.org/) and [Rubinius](http://rubini.us/) but GitLab
diff --git a/doc/integration/README.md b/doc/integration/README.md
index ddbd570ac6c..c2fd299db07 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -15,6 +15,7 @@ See the documentation below for details on how to configure these services.
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
+- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
diff --git a/doc/integration/bitbucket.md b/doc/integration/bitbucket.md
index 2eb6266ebe7..556d71b8b76 100644
--- a/doc/integration/bitbucket.md
+++ b/doc/integration/bitbucket.md
@@ -1,111 +1,164 @@
-# Integrate your server with Bitbucket
+# Integrate your GitLab server with Bitbucket
-Import projects from Bitbucket and login to your GitLab instance with your Bitbucket account.
+Import projects from Bitbucket.org and login to your GitLab instance with your
+Bitbucket.org account.
-To enable the Bitbucket OmniAuth provider you must register your application with Bitbucket.
-Bitbucket will generate an application ID and secret key for you to use.
+## Overview
-1. Sign in to Bitbucket.
+You can set up Bitbucket.org as an OAuth provider so that you can use your
+credentials to authenticate into GitLab or import your projects from
+Bitbucket.org.
-1. Navigate to your individual user settings or a team's settings, depending on how you want the application registered. It does not matter if the application is registered as an individual or a team - that is entirely up to you.
+- To use Bitbucket.org as an OmniAuth provider, follow the [Bitbucket OmniAuth
+ provider](#bitbucket-omniauth-provider) section.
+- To import projects from Bitbucket, follow both the
+ [Bitbucket OmniAuth provider](#bitbucket-omniauth-provider) and
+ [Bitbucket project import](#bitbucket-project-import) sections.
-1. Select "OAuth" in the left menu.
+## Bitbucket OmniAuth provider
-1. Select "Add consumer".
+> **Note:**
+Make sure to first follow the [Initial OmniAuth configuration][init-oauth]
+before proceeding with setting up the Bitbucket integration.
-1. Provide the required details.
- - Name: This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive.
- - Application description: Fill this in if you wish.
- - URL: The URL to your GitLab installation. 'https://gitlab.company.com'
-1. Select "Save".
+To enable the Bitbucket OmniAuth provider you must register your application
+with Bitbucket.org. Bitbucket will generate an application ID and secret key for
+you to use.
-1. You should now see a Key and Secret in the list of OAuth customers.
- Keep this page open as you continue configuration.
+1. Sign in to [Bitbucket.org](https://bitbucket.org).
+1. Navigate to your individual user settings (**Bitbucket settings**) or a team's
+ settings (**Manage team**), depending on how you want the application registered.
+ It does not matter if the application is registered as an individual or a
+ team, that is entirely up to you.
+1. Select **OAuth** in the left menu under "Access Management".
+1. Select **Add consumer**.
+1. Provide the required details:
-1. On your GitLab server, open the configuration file.
+ | Item | Description |
+ | :--- | :---------- |
+ | **Name** | This can be anything. Consider something like `<Organization>'s GitLab` or `<Your Name>'s GitLab` or something else descriptive. |
+ | **Application description** | Fill this in if you wish. |
+ | **Callback URL** | Leave blank. |
+ | **URL** | The URL to your GitLab installation, e.g., `https://gitlab.example.com`. |
- For omnibus package:
+ And grant at least the following permissions:
- ```sh
- sudo editor /etc/gitlab/gitlab.rb
+ ```
+ Account: Email
+ Repositories: Read, Admin
```
- For installations from source:
+ >**Note:**
+ It may seem a little odd to giving GitLab admin permissions to repositories,
+ but this is needed in order for GitLab to be able to clone the repositories.
- ```sh
- cd /home/git/gitlab
+ ![Bitbucket OAuth settings page](img/bitbucket_oauth_settings_page.png)
+
+1. Select **Save**.
+1. Select your newly created OAuth consumer and you should now see a Key and
+ Secret in the list of OAuth customers. Keep this page open as you continue
+ the configuration.
+
+ ![Bitbucket OAuth key](img/bitbucket_oauth_keys.png)
+
+1. On your GitLab server, open the configuration file:
- sudo -u git -H editor config/gitlab.yml
```
+ # For Omnibus packages
+ sudo editor /etc/gitlab/gitlab.rb
-1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+ # For installations from source
+ sudo -u git -H editor /home/git/gitlab/config/gitlab.yml
+ ```
-1. Add the provider configuration:
+1. Follow the [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration)
+ for initial settings.
+1. Add the Bitbucket provider configuration:
- For omnibus package:
+ For Omnibus packages:
```ruby
- gitlab_rails['omniauth_providers'] = [
- {
- "name" => "bitbucket",
- "app_id" => "YOUR_KEY",
- "app_secret" => "YOUR_APP_SECRET",
- "url" => "https://bitbucket.org/"
- }
- ]
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "bitbucket",
+ "app_id" => "BITBUCKET_APP_KEY",
+ "app_secret" => "BITBUCKET_APP_SECRET",
+ "url" => "https://bitbucket.org/"
+ }
+ ]
```
- For installation from source:
+ For installations from source:
- ```
- - { name: 'bitbucket', app_id: 'YOUR_KEY',
- app_secret: 'YOUR_APP_SECRET' }
+ ```yaml
+ - { name: 'bitbucket',
+ app_id: 'BITBUCKET_APP_KEY',
+ app_secret: 'BITBUCKET_APP_SECRET' }
```
-1. Change 'YOUR_APP_ID' to the key from the Bitbucket application page from step 7.
+ ---
-1. Change 'YOUR_APP_SECRET' to the secret from the Bitbucket application page from step 7.
+ Where `BITBUCKET_APP_KEY` is the Key and `BITBUCKET_APP_SECRET` the Secret
+ from the Bitbucket application page.
1. Save the configuration file.
+1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
-1. If you're using the omnibus package, reconfigure GitLab (```gitlab-ctl reconfigure```).
-
-1. Restart GitLab for the changes to take effect.
-
-On the sign in page there should now be a Bitbucket icon below the regular sign in form.
-Click the icon to begin the authentication process. Bitbucket will ask the user to sign in and authorize the GitLab application.
-If everything goes well the user will be returned to GitLab and will be signed in.
+On the sign in page there should now be a Bitbucket icon below the regular sign
+in form. Click the icon to begin the authentication process. Bitbucket will ask
+the user to sign in and authorize the GitLab application. If everything goes
+well, the user will be returned to GitLab and will be signed in.
## Bitbucket project import
-To allow projects to be imported directly into GitLab, Bitbucket requires two extra setup steps compared to GitHub and GitLab.com.
+To allow projects to be imported directly into GitLab, Bitbucket requires two
+extra setup steps compared to [GitHub](github.md) and [GitLab.com](gitlab.md).
-Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and instead requires GitLab to use SSH and identify itself using your GitLab server's SSH key.
+Bitbucket doesn't allow OAuth applications to clone repositories over HTTPS, and
+instead requires GitLab to use SSH and identify itself using your GitLab
+server's SSH key.
-### Step 1: Public key
+To be able to access repositories on Bitbucket, GitLab will automatically
+register your public key with Bitbucket as a deploy key for the repositories to
+be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa` which
+translates to `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages and to
+`/home/git/.ssh/bitbucket_rsa.pub` for installations from source.
-To be able to access repositories on Bitbucket, GitLab will automatically register your public key with Bitbucket as a deploy key for the repositories to be imported. Your public key needs to be at `~/.ssh/bitbucket_rsa.pub`, which will expand to `/home/git/.ssh/bitbucket_rsa.pub` in most configurations.
+---
-If you have that file in place, you're all set and should see the "Import projects from Bitbucket" option enabled. If you don't, do the following:
+Below are the steps that will allow GitLab to be able to import your projects
+from Bitbucket.
-1. Create a new SSH key:
+1. Make sure you [have enabled the Bitbucket OAuth support](#bitbucket-omniauth-provider).
+1. Create a new SSH key with an **empty passphrase**:
```sh
sudo -u git -H ssh-keygen
```
- When asked `Enter file in which to save the key` specify the correct path, eg. `/home/git/.ssh/bitbucket_rsa`.
- Make sure to use an **empty passphrase**.
+ When asked to 'Enter file in which to save the key' enter:
+ `/var/opt/gitlab/.ssh/bitbucket_rsa` for Omnibus packages or
+ `/home/git/.ssh/bitbucket_rsa` for installations from source. The name is
+ important so make sure to get it right.
-1. Configure SSH client to use your new key:
+ > **Warning:**
+ This key must NOT be associated with ANY existing Bitbucket accounts. If it
+ is, the import will fail with an `Access denied! Please verify you can add
+ deploy keys to this repository.` error.
- Open the SSH configuration file of the git user.
+1. Next, you need to to configure the SSH client to use your new key. Open the
+ SSH configuration file of the `git` user:
- ```sh
- sudo editor /home/git/.ssh/config
+ ```
+ # For Omnibus packages
+ sudo editor /var/opt/gitlab/.ssh/config
+
+ # For installations from source
+ sudo editor /home/git/.ssh/config
```
- Add a host configuration for `bitbucket.org`.
+1. Add a host configuration for `bitbucket.org`:
```sh
Host bitbucket.org
@@ -113,28 +166,46 @@ If you have that file in place, you're all set and should see the "Import projec
User git
```
-### Step 2: Known hosts
-
-To allow GitLab to connect to Bitbucket over SSH, you need to add 'bitbucket.org' to your GitLab server's known SSH hosts. Take the following steps to do so:
-
-1. Manually connect to 'bitbucket.org' over SSH, while logged in as the `git` account that GitLab will use:
+1. Save the file and exit.
+1. Manually connect to `bitbucket.org` over SSH, while logged in as the `git`
+ user that GitLab will use:
```sh
sudo -u git -H ssh bitbucket.org
```
-1. Verify the RSA key fingerprint you'll see in the response matches the one in the [Bitbucket documentation](https://confluence.atlassian.com/display/BITBUCKET/Use+the+SSH+protocol+with+Bitbucket#UsetheSSHprotocolwithBitbucket-KnownhostorBitbucket'spublickeyfingerprints) (the specific IP address doesn't matter):
+ That step is performed because GitLab needs to connect to Bitbucket over SSH,
+ in order to add `bitbucket.org` to your GitLab server's known SSH hosts.
+
+1. Verify the RSA key fingerprint you'll see in the response matches the one
+ in the [Bitbucket documentation][bitbucket-docs] (the specific IP address
+ doesn't matter):
```sh
- The authenticity of host 'bitbucket.org (207.223.240.182)' can't be established.
- RSA key fingerprint is 97:8c:1b:f2:6f:14:6b:5c:3b:ec:aa:46:46:74:7c:40.
+ The authenticity of host 'bitbucket.org (104.192.143.1)' can't be established.
+ RSA key fingerprint is SHA256:zzXQOXSRBEiUtuE8AikJYKwbHaxvSc0ojez9YXaGp1A.
Are you sure you want to continue connecting (yes/no)?
```
-1. If the fingerprint matches, type `yes` to continue connecting and have 'bitbucket.org' be added to your known hosts.
+1. If the fingerprint matches, type `yes` to continue connecting and have
+ `bitbucket.org` be added to your known SSH hosts. After confirming you should
+ see a permission denied message. If you see an authentication successful
+ message you have done something wrong. The key you are using has already been
+ added to a Bitbucket account and will cause the import script to fail. Ensure
+ the key you are using CANNOT authenticate with Bitbucket.
+1. Restart GitLab to allow it to find the new public key.
-1. Your GitLab server is now able to connect to Bitbucket over SSH.
+Your GitLab server is now able to connect to Bitbucket over SSH. You should be
+able to see the "Import projects from Bitbucket" option on the New Project page
+enabled.
-1. Restart GitLab to allow it to find the new public key.
+## Acknowledgemts
+
+Special thanks to the writer behind the following article:
+
+- http://stratus3d.com/blog/2015/09/06/migrating-from-bitbucket-to-local-gitlab-server/
-You should now see the "Import projects from Bitbucket" option on the New Project page enabled.
+[init-oauth]: omniauth.md#initial-omniauth-configuration
+[bitbucket-docs]: https://confluence.atlassian.com/bitbucket/use-the-ssh-protocol-with-bitbucket-cloud-221449711.html#UsetheSSHprotocolwithBitbucketCloud-KnownhostorBitbucket%27spublickeyfingerprints
+[reconfigure]: ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure
+[restart GitLab]: ../administration/restart_gitlab.md#installations-from-source
diff --git a/doc/integration/img/bitbucket_oauth_keys.png b/doc/integration/img/bitbucket_oauth_keys.png
new file mode 100644
index 00000000000..3fb2f7524a3
--- /dev/null
+++ b/doc/integration/img/bitbucket_oauth_keys.png
Binary files differ
diff --git a/doc/integration/img/bitbucket_oauth_settings_page.png b/doc/integration/img/bitbucket_oauth_settings_page.png
new file mode 100644
index 00000000000..a3047712d8c
--- /dev/null
+++ b/doc/integration/img/bitbucket_oauth_settings_page.png
Binary files differ
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index 46b260e7033..8a55fce96fe 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -102,8 +102,8 @@ To change these settings:
block_auto_created_users: true
```
-Now we can choose one or more of the Supported Providers listed above to continue
-the configuration process.
+Now we can choose one or more of the [Supported Providers](#supported-providers)
+listed above to continue the configuration process.
## Enable OmniAuth for an Existing User
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index 84c624cbcb7..b058f8e2a03 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -20,7 +20,31 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
-### 3. Get latest code
+### 3. Update Ruby
+
+If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
```bash
sudo -u git -H git fetch --all
@@ -41,15 +65,15 @@ For GitLab Enterprise Edition:
sudo -u git -H git checkout 8-11-stable-ee
```
-### 4. Update gitlab-shell
+### 5. Update gitlab-shell
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.3.3
+sudo -u git -H git checkout v3.4.0
```
-### 5. Update gitlab-workhorse
+### 6. Update gitlab-workhorse
Install and compile gitlab-workhorse. This requires
[Go 1.5](https://golang.org/dl) which should already be on your system from
@@ -58,11 +82,11 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v0.7.8
+sudo -u git -H git checkout v0.7.11
sudo -u git -H make
```
-### 6. Install libs, migrations, etc.
+### 7. Install libs, migrations, etc.
```bash
cd /home/git/gitlab
@@ -84,7 +108,7 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
```
-### 7. Update configuration files
+### 8. Update configuration files
#### New configuration options for `gitlab.yml`
@@ -133,12 +157,12 @@ Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
-### 8. Start application
+### 9. Start application
sudo service gitlab start
sudo service nginx restart
-### 9. Check application status
+### 10. Check application status
Check if GitLab and its environment are configured correctly:
diff --git a/doc/user/account/security.md b/doc/user/account/security.md
new file mode 100644
index 00000000000..816094bf8d2
--- /dev/null
+++ b/doc/user/account/security.md
@@ -0,0 +1,3 @@
+# Account Security
+
+- [Two-Factor Authentication](two_factor_authentication.md)
diff --git a/doc/user/account/two_factor_authentication.md b/doc/user/account/two_factor_authentication.md
new file mode 100644
index 00000000000..881358ed94d
--- /dev/null
+++ b/doc/user/account/two_factor_authentication.md
@@ -0,0 +1,68 @@
+# Two-Factor Authentication
+
+## Recovery options
+
+If you lose your code generation device (such as your mobile phone) and you need
+to disable two-factor authentication on your account, you have several options.
+
+### Use a saved recovery code
+
+When you enabled two-factor authentication for your account, a series of
+recovery codes were generated. If you saved those codes somewhere safe, you
+may use one to sign in.
+
+First, enter your username/email and password on the GitLab sign in page. When
+prompted for a two-factor code, enter one of the recovery codes you saved
+previously.
+
+> **Note:** Once a particular recovery code has been used, it cannot be used again.
+ You may still use the other saved recovery codes at a later time.
+
+### Generate new recovery codes using SSH
+
+It's not uncommon for users to forget to save the recovery codes when enabling
+two-factor authentication. If you have an SSH key added to your GitLab account,
+you can generate a new set of recovery codes using SSH.
+
+Run `ssh git@gitlab.example.com 2fa_recovery_codes`. You will be prompted to
+confirm that you wish to generate new codes. If you choose to continue, any
+previously saved codes will be invalidated.
+
+```bash
+$ ssh git@gitlab.example.com 2fa_recovery_codes
+Are you sure you want to generate new two-factor recovery codes?
+Any existing recovery codes you saved will be invalidated. (yes/no)
+yes
+
+Your two-factor authentication recovery codes are:
+
+119135e5a3ebce8e
+11f6v2a498810dcd
+3924c7ab2089c902
+e79a3398bfe4f224
+34bd7b74adbc8861
+f061691d5107df1a
+169bf32a18e63e7f
+b510e7422e81c947
+20dbed24c5e74663
+df9d3b9403b9c9f0
+
+During sign in, use one of the codes above when prompted for
+your two-factor code. Then, visit your Profile Settings and add
+a new device so you do not lose access to your account again.
+```
+
+Next, go to the GitLab sign in page and enter your username/email and password.
+When prompted for a two-factor code, enter one of the recovery codes obtained
+from the command line output.
+
+> **Note:** After signing in, you should immediately visit your **Profile Settings
+ -> Account** to set up two-factor authentication with a new device.
+
+### Ask a GitLab administrator to disable two-factor on your account
+
+If the above two methods are not possible, you may ask a GitLab global
+administrator to disable two-factor authentication for your account. Please
+be aware that this will temporarily leave your account in a less secure state.
+You should sign in and re-enable two-factor authentication as soon as possible
+after the administrator disables it.
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 7fe96e67dbb..c7fda8a497f 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -66,7 +66,7 @@ dependency to do so. Please see the [github-markup gem readme](https://github.co
## Newlines
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#newlines
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#newlines
GFM honors the markdown specification in how [paragraphs and line breaks are handled](https://daringfireball.net/projects/markdown/syntax#p).
@@ -86,7 +86,7 @@ Sugar is sweet
## Multiple underscores in words
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiple-underscores-in-words
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiple-underscores-in-words
It is not reasonable to italicize just _part_ of a word, especially when you're dealing with code and names that often appear with multiple underscores. Therefore, GFM ignores multiple underscores in words:
@@ -101,7 +101,7 @@ do_this_and_do_that_and_another_thing
## URL auto-linking
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#url-auto-linking
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#url-auto-linking
GFM will autolink almost any URL you copy and paste into your text:
@@ -122,7 +122,7 @@ GFM will autolink almost any URL you copy and paste into your text:
## Multiline Blockquote
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#multiline-blockquote
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#multiline-blockquote
On top of standard Markdown [blockquotes](#blockquotes), which require prepending `>` to quoted lines,
GFM supports multiline blockquotes fenced by <code>>>></code>:
@@ -156,7 +156,7 @@ you can quote that without having to manually prepend `>` to every line!
## Code and Syntax Highlighting
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#code-and-syntax-highlighting
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#code-and-syntax-highlighting
_GitLab uses the [Rouge Ruby library][rouge] for syntax highlighting. For a
list of supported languages visit the Rouge website._
@@ -226,7 +226,7 @@ But let's throw in a <b>tag</b>.
## Inline Diff
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#inline-diff
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#inline-diff
With inline diffs tags you can display {+ additions +} or [- deletions -].
@@ -242,7 +242,7 @@ However the wrapping tags cannot be mixed as such:
## Emoji
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#emoji
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
@@ -307,7 +307,7 @@ GFM also recognizes certain cross-project references:
## Task Lists
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#task-lists
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#task-lists
You can add task lists to issues, merge requests and comments. To create a task list, add a specially-formatted Markdown list, like so:
@@ -330,7 +330,7 @@ Task lists can only be created in descriptions, not in titles. Task item state c
## Videos
> If this is not rendered correctly, see
-https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md#videos
+https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#videos
Image tags with a video extension are automatically converted to a video player.
@@ -780,7 +780,7 @@ A link starting with a `/` is relative to the wiki root.
- The [Markdown Syntax Guide](https://daringfireball.net/projects/markdown/syntax) at Daring Fireball is an excellent resource for a detailed explanation of standard markdown.
- [Dillinger.io](http://dillinger.io) is a handy tool for testing standard markdown.
-[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/markdown/markdown.md
+[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[^1]: This link will be broken if you see this document from the Help page or docs.gitlab.com
diff --git a/doc/user/project/img/issue_board.png b/doc/user/project/img/issue_board.png
new file mode 100644
index 00000000000..63c269f6dbc
--- /dev/null
+++ b/doc/user/project/img/issue_board.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_add_list.png b/doc/user/project/img/issue_board_add_list.png
new file mode 100644
index 00000000000..2b8c10eaa0a
--- /dev/null
+++ b/doc/user/project/img/issue_board_add_list.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_search_backlog.png b/doc/user/project/img/issue_board_search_backlog.png
new file mode 100644
index 00000000000..112ea171539
--- /dev/null
+++ b/doc/user/project/img/issue_board_search_backlog.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_system_notes.png b/doc/user/project/img/issue_board_system_notes.png
new file mode 100644
index 00000000000..b69ef034954
--- /dev/null
+++ b/doc/user/project/img/issue_board_system_notes.png
Binary files differ
diff --git a/doc/user/project/img/issue_board_welcome_message.png b/doc/user/project/img/issue_board_welcome_message.png
new file mode 100644
index 00000000000..b757faeb230
--- /dev/null
+++ b/doc/user/project/img/issue_board_welcome_message.png
Binary files differ
diff --git a/doc/user/project/img/koding_build-in-progress.png b/doc/user/project/img/koding_build-in-progress.png
new file mode 100644
index 00000000000..f8cc81834c4
--- /dev/null
+++ b/doc/user/project/img/koding_build-in-progress.png
Binary files differ
diff --git a/doc/user/project/img/koding_build-logs.png b/doc/user/project/img/koding_build-logs.png
new file mode 100644
index 00000000000..a04cd5aff99
--- /dev/null
+++ b/doc/user/project/img/koding_build-logs.png
Binary files differ
diff --git a/doc/user/project/img/koding_build-success.png b/doc/user/project/img/koding_build-success.png
new file mode 100644
index 00000000000..2a0dd296480
--- /dev/null
+++ b/doc/user/project/img/koding_build-success.png
Binary files differ
diff --git a/doc/user/project/img/koding_commit-koding.yml.png b/doc/user/project/img/koding_commit-koding.yml.png
new file mode 100644
index 00000000000..3e133c50327
--- /dev/null
+++ b/doc/user/project/img/koding_commit-koding.yml.png
Binary files differ
diff --git a/doc/user/project/img/koding_different-stack-on-mr-try.png b/doc/user/project/img/koding_different-stack-on-mr-try.png
new file mode 100644
index 00000000000..fd25e32f648
--- /dev/null
+++ b/doc/user/project/img/koding_different-stack-on-mr-try.png
Binary files differ
diff --git a/doc/user/project/img/koding_edit-on-ide.png b/doc/user/project/img/koding_edit-on-ide.png
new file mode 100644
index 00000000000..fd5aaff75f5
--- /dev/null
+++ b/doc/user/project/img/koding_edit-on-ide.png
Binary files differ
diff --git a/doc/user/project/img/koding_enable-koding.png b/doc/user/project/img/koding_enable-koding.png
new file mode 100644
index 00000000000..c0ae0ee9918
--- /dev/null
+++ b/doc/user/project/img/koding_enable-koding.png
Binary files differ
diff --git a/doc/user/project/img/koding_landing.png b/doc/user/project/img/koding_landing.png
new file mode 100644
index 00000000000..7c629d9b05e
--- /dev/null
+++ b/doc/user/project/img/koding_landing.png
Binary files differ
diff --git a/doc/user/project/img/koding_open-gitlab-from-koding.png b/doc/user/project/img/koding_open-gitlab-from-koding.png
new file mode 100644
index 00000000000..c958cf8f224
--- /dev/null
+++ b/doc/user/project/img/koding_open-gitlab-from-koding.png
Binary files differ
diff --git a/doc/user/project/img/koding_run-in-ide.png b/doc/user/project/img/koding_run-in-ide.png
new file mode 100644
index 00000000000..f91ee0f74cc
--- /dev/null
+++ b/doc/user/project/img/koding_run-in-ide.png
Binary files differ
diff --git a/doc/user/project/img/koding_run-mr-in-ide.png b/doc/user/project/img/koding_run-mr-in-ide.png
new file mode 100644
index 00000000000..502817a2a46
--- /dev/null
+++ b/doc/user/project/img/koding_run-mr-in-ide.png
Binary files differ
diff --git a/doc/user/project/img/koding_set-up-ide.png b/doc/user/project/img/koding_set-up-ide.png
new file mode 100644
index 00000000000..7f408c980b5
--- /dev/null
+++ b/doc/user/project/img/koding_set-up-ide.png
Binary files differ
diff --git a/doc/user/project/img/koding_stack-import.png b/doc/user/project/img/koding_stack-import.png
new file mode 100644
index 00000000000..2a4e3c87fc8
--- /dev/null
+++ b/doc/user/project/img/koding_stack-import.png
Binary files differ
diff --git a/doc/user/project/img/koding_start-build.png b/doc/user/project/img/koding_start-build.png
new file mode 100644
index 00000000000..52159440f62
--- /dev/null
+++ b/doc/user/project/img/koding_start-build.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_devs_can_push.png b/doc/user/project/img/protected_branches_devs_can_push.png
index 9c33db36586..812cc8767b7 100644
--- a/doc/user/project/img/protected_branches_devs_can_push.png
+++ b/doc/user/project/img/protected_branches_devs_can_push.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_list.png b/doc/user/project/img/protected_branches_list.png
index 9f070f7a208..f33f1b2bdb6 100644
--- a/doc/user/project/img/protected_branches_list.png
+++ b/doc/user/project/img/protected_branches_list.png
Binary files differ
diff --git a/doc/user/project/img/protected_branches_page.png b/doc/user/project/img/protected_branches_page.png
new file mode 100644
index 00000000000..1585dde5b29
--- /dev/null
+++ b/doc/user/project/img/protected_branches_page.png
Binary files differ
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
new file mode 100644
index 00000000000..cac926b3e28
--- /dev/null
+++ b/doc/user/project/issue_board.md
@@ -0,0 +1,187 @@
+# Issue board
+
+> [Introduced][ce-5554] in GitLab 8.11.
+
+The GitLab Issue Board is a software project management tool used to plan,
+organize, and visualize a workflow for a feature or product release.
+It can be seen like a light version of a [Kanban] or a [Scrum] board.
+
+Other interesting links:
+
+- [GitLab Issue Board landing page on about.gitlab.com][landing]
+- [YouTube video introduction to Issue Boards][youtube]
+
+## Overview
+
+The Issue Board builds on GitLab's existing issue tracking functionality and
+leverages the power of [labels] by utilizing them as lists of the scrum board.
+
+With the Issue Board you can have a different view of your issues while also
+maintaining the same filtering and sorting abilities you see across the
+issue tracker.
+
+Below is a table of the definitions used for GitLab's Issue Board.
+
+| What we call it | What it means |
+| -------------- | ------------- |
+| **Issue Board** | It represents a different view for your issues. It can have multiple lists with each list consisting of issues represented by cards. |
+| **List** | Each label that exists in the issue tracker can have its own dedicated list. Every list is named after the label it is based on and is represented by a column which contains all the issues associated with that label. You can think of a list like the results you get when you filter the issues by a label in your issue tracker. |
+| **Card** | Every card represents an issue and it is shown under the list for which it has a label. The information you can see on a card consists of the issue number, the issue title, the assignee and the labels associated with it. You can drag cards around from one list to another. Issues inside lists are [ordered by priority](labels.md#prioritize-labels). |
+
+There are three types of lists, the ones you create based on your labels, and
+two default:
+
+- **Backlog** (default): shows all opened issues that do not fall in one of the other lists. Always appears on the very left.
+- **Done** (default): shows all closed issues that do not fall in one of the other lists. Always appears on the very right.
+- Label list: a list based on a label. It shows all opened or closed issues with that label.
+
+![GitLab Issue Board](img/issue_board.png)
+
+---
+
+In short, here's a list of actions you can take in an Issue Board:
+
+- [Create a new list](#creating-a-new-list).
+- [Delete an existing list](#deleting-a-list).
+- Drag issues between lists.
+- Drag and reorder the lists themselves.
+- Change issue labels on-the-fly while dragging issues between lists.
+- Close an issue if you drag it to the **Done** list.
+- Create a new list from a non-existing label by [creating the label on-the-fly](#creating-a-new-list)
+ within the Issue Board.
+- [Filter issues](#filtering-issues) that appear across your Issue Board.
+
+If you are not able to perform one or more of the things above, make sure you
+have the right [permissions](#permissions).
+
+## First time using the Issue Board
+
+The first time you navigate to your Issue Board, you will be presented with the
+two default lists (**Backlog** and **Done**) and a welcoming message that gives
+you two options. You can either create a predefined set of labels and create
+their corresponding lists to the Issue Board or opt-out and use your own lists.
+
+![Issue Board welcome message](img/issue_board_welcome_message.png)
+
+If you choose to use and create the predefined lists, they will appear as empty
+because the labels associated to them will not exist up until that moment,
+which means the system has no way of populating them automatically. That's of
+course if the predefined labels don't already exist. If any of them does exist,
+the list will be created and filled with the issues that have that label.
+
+## Creating a new list
+
+Create a new list by clicking on the **Create new list** button at the upper
+right corner of the Issue Board.
+
+![Issue Board welcome message](img/issue_board_add_list.png)
+
+Simply choose the label to create the list from. The new list will be inserted
+at the end of the lists, before **Done**. Moving and reordering lists is as
+easy as dragging them around.
+
+To create a list for a label that doesn't yet exist, simply create the label by
+choosing **Create new label**. The label will be created on-the-fly and it will
+be immediately added to the dropdown. You can now choose it to create a list.
+
+## Deleting a list
+
+To delete a list from the Issue Board use the small trash icon that is present
+in the list's heading. A confirmation dialog will appear for you to confirm.
+
+Deleting a list doesn't have any effect in issues and labels, it's just the
+list view that is removed. You can always add it back later if you need.
+
+## Searching issues in the Backlog list
+
+The very first time you start using the Issue Board, it is very likely your
+issue tracker is already populated with labels and issues. In that case,
+**Backlog** will have all the issues that don't belong to another list, and
+**Done** will have all the closed ones.
+
+For performance and visibility reasons, each list shows the first 20 issues
+by default. If you have more than 20, you have to start scrolling down for the
+next 20 issues to appear. This can be cumbersome if your issue tracker hosts
+hundreds of issues, and for that reason it is easier to search for issues to
+move from **Backlog** to another list.
+
+Start typing in the search bar under the **Backlog** list and the relevant
+issues will appear.
+
+![Issue Board search Backlog](img/issue_board_search_backlog.png)
+
+## Filtering issues
+
+You should be able to use the filters on top of your Issue Board to show only
+the results you want. This is similar to the filtering used in the issue tracker
+since the metadata from the issues and labels are re-used in the Issue Board.
+
+You can filter by author, assignee, milestone and label.
+
+## Creating workflows
+
+By reordering your lists, you can create workflows. As lists in Issue Boards are
+based on labels, it works out of the box with your existing issues. So if you've
+already labeled things with 'Backend' and 'Frontend', the issue will appear in
+the lists as you create them. In addition, this means you can easily move
+something between lists by changing a label.
+
+A typical workflow of using the Issue Board would be:
+
+1. You have [created][create-labels] and [prioritized][label-priority] labels
+ so that you can easily categorize your issues.
+1. You have a bunch of issues (ideally labeled).
+1. You visit the Issue Board and start [creating lists](#creating-a-new-list) to
+ create a workflow.
+1. You move issues around in lists so that your team knows who should be working
+ on what issue.
+1. When the work by one team is done, the issue can be dragged to the next list
+ so someone else can pick up.
+1. When the issue is finally resolved, the issue is moved to the **Done** list
+ and gets automatically closed.
+
+For instance you can create a list based on the label of 'Frontend' and one for
+'Backend'. A designer can start working on an issue by dragging it from
+**Backlog** to 'Frontend'. That way, everyone knows that this issue is now being
+worked on by the designers. Then, once they're done, all they have to do is
+drag it over to the next list, 'Backend', where a backend developer can
+eventually pick it up. Once they’re done, they move it to **Done**, to close the
+issue.
+
+This process can be seen clearly when visiting an issue since with every move
+to another list the label changes and a system not is recorded.
+
+![Issue Board system notes](img/issue_board_system_notes.png)
+
+## Permissions
+
+[Developers and up](../permissions.md) can use all the functionality of the
+Issue Board, that is create/delete lists and drag issues around.
+
+## Tips
+
+A few things to remember:
+
+- The label that corresponds to a list is hidden for issues under that list.
+- Moving an issue between lists removes the label from the list it came from
+ and adds the label from the list it goes to.
+- When moving a card to **Done**, the label of the list it came from is removed
+ and the issue gets closed.
+- An issue can exist in multiple lists if it has more than one label.
+- Lists are populated with issues automatically if the issues are labeled.
+- Clicking on the issue title inside a card will take you to that issue.
+- Clicking on a label inside a card will quickly filter the entire Issue Board
+ and show only the issues from all lists that have that label.
+- Issues inside lists are [ordered by priority][label-priority].
+- For performance and visibility reasons, each list shows the first 20 issues
+ by default. If you have more than 20 issues start scrolling down and the next
+ 20 will appear.
+
+[ce-5554]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5554
+[labels]: ./labels.md
+[scrum]: https://en.wikipedia.org/wiki/Scrum_(software_development)
+[kanban]: https://en.wikipedia.org/wiki/Kanban_(development)
+[create-labels]: ./labels.md#create-new-labels
+[label-priority]: ./labels.md#prioritize-labels
+[landing]: https://about.gitlab.com/solutions/issueboard
+[youtube]: https://www.youtube.com/watch?v=UWsJ8tkHAa8
diff --git a/doc/user/project/koding.md b/doc/user/project/koding.md
new file mode 100644
index 00000000000..c56a1efe3c2
--- /dev/null
+++ b/doc/user/project/koding.md
@@ -0,0 +1,128 @@
+# Koding & GitLab
+
+> [Introduced][ce-5909] in GitLab 8.11.
+
+This document will guide you through using Koding integration on GitLab in
+detail. For configuring and installing please follow the
+[administrator guide](../../administration/integration/koding.md).
+
+You can use Koding integration to run and develop your projects on GitLab. This
+will allow you and the users to test your project without leaving the browser.
+Koding handles projects as stacks which are basic recipes to define your
+environment for your project. With this integration you can automatically
+create a proper stack template for your projects. Currently auto-generated
+stack templates are designed to work with AWS which requires a valid AWS
+credential to be able to use these stacks. You can find more information about
+stacks and the other providers that you can use on Koding following the
+[Koding documentation][koding-docs].
+
+## Enable Integration
+
+You can enable Koding integration by providing the running Koding instance URL
+in Application Settings under **Admin area > Settings** (`/admin/application_settings`).
+
+![Enable Koding](img/koding_enable-koding.png)
+
+Once enabled you will see `Koding` link on your sidebar which leads you to
+Koding Landing page.
+
+![Koding Landing](img/koding_landing.png)
+
+You can navigate to running Koding instance from here. For more information and
+details about configuring the integration, please follow the
+[administrator guide](../../administration/integration/koding.md).
+
+## Set up Koding on Projects
+
+Once it's enabled, you will see some integration buttons on Project pages,
+Merge Requests etc. To get started working on a specific project you first need
+to create a `.koding.yml` file under your project root. You can easily do that
+by using `Set Up Koding` button which will be visible on every project's
+landing page;
+
+![Set Up Koding](img/koding_set-up-ide.png)
+
+Once you click this will open a New File page on GitLab with auto-generated
+`.koding.yml` content based on your server and repository configuration.
+
+![Commit .koding.yml](img/koding_commit-koding.yml.png)
+
+
+## Run a project on Koding
+
+If there is `.koding.yml` exists in your project root, you will see
+`Run in IDE (Koding)` button in your project landing page. You can initiate the
+process from here.
+
+![Run on Koding](img/koding_run-in-ide.png)
+
+This will open Koding defined in the settings in a new window and will start
+importing the project's stack file.
+
+![Import Stack](img/koding_stack-import.png)
+
+You should see the details of your repository imported into your Koding
+instance. Once it's completed it will lead you to the Stack Editor and from
+there you can start using your new stack integrated with your project on your
+GitLab instance. For details about what's next you can follow
+[this guide](https://www.koding.com/docs/creating-an-aws-stack) from step 8.
+
+Once stack initialized you will see the `README.md` content from your project
+in `Stack Build` wizard, this wizard will let you build the stack and import
+your project into it. **Once it's completed it will automatically open the
+related vm instead of importing from scratch**.
+
+![Stack Building](img/koding_start-build.png)
+
+This will take time depending on the required environment.
+
+![Stack Building in Progress](img/koding_build-in-progress.png)
+
+It usually takes ~4 min. to make it ready with a `t2.nano` instance on given
+AWS region. (`t2.nano` is default vm type on auto-generated stack template
+which can be manually changed).
+
+![Stack Building Success](img/koding_build-success.png)
+
+You can check out the `Build Logs` from this success modal as well.
+
+![Stack Build Logs](img/koding_build-logs.png)
+
+You can now `Start Coding`!
+
+![Edit On IDE](img/koding_edit-on-ide.png)
+
+## Try a Merge Request on IDE
+
+It's also possible to try a change on IDE before merging it. This flow only
+enabled if the target project has `.koding.yml` in it's target branch. You
+should see the alternative version of `Run in IDE (Koding)` button in merge
+request pages as well;
+
+![Run in IDE on MR](img/koding_run-mr-in-ide.png)
+
+This will again take you to Koding with proper arguments passed, which will
+allow Koding to modify the stack template provided by target branch. You can
+see the difference;
+
+![Different Branch for MR](img/koding_different-stack-on-mr-try.png)
+
+The flow for the branch stack is also same with the regular project flow.
+
+## Open GitLab from Koding
+
+Since stacks generated with import flow defined in previous steps, they have
+information about the repository they are belonging to. By using this
+information you can access to related GitLab page from stacks on your sidebar
+on Koding.
+
+![Open GitLab from Koding](img/koding_open-gitlab-from-koding.png)
+
+## Other links
+
+- [YouTube video on GitLab + Koding workflow][youtube]
+- [Koding documentation][koding-docs]
+
+[ce-5909]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5909
+[youtube]: https://youtu.be/3wei5yv_Ye8
+[koding-docs]: https://www.koding.com/docs
diff --git a/doc/user/project/merge_requests/resolve_conflicts.md b/doc/user/project/merge_requests/resolve_conflicts.md
index 44b76ffc8e6..4d7225bd820 100644
--- a/doc/user/project/merge_requests/resolve_conflicts.md
+++ b/doc/user/project/merge_requests/resolve_conflicts.md
@@ -26,6 +26,7 @@ this is similar to performing `git checkout feature; git merge master` locally.
GitLab allows resolving conflicts in a file where all of the below are true:
- The file is text, not binary
+- The file is in a UTF-8 compatible encoding
- The file does not already contain conflict markers
- The file, with conflict markers added, is not over 200 KB in size
- The file exists under the same path in both branches
diff --git a/doc/user/project/protected_branches.md b/doc/user/project/protected_branches.md
index 96d9bdc1b29..f7a686d2ccf 100644
--- a/doc/user/project/protected_branches.md
+++ b/doc/user/project/protected_branches.md
@@ -5,6 +5,8 @@ idea of having read or write permission to the repository and branches. To
prevent people from messing with history or pushing code without review, we've
created protected branches.
+## Overview
+
By default, a protected branch does four simple things:
- it prevents its creation, if not already created, from everybody except users
@@ -15,6 +17,11 @@ By default, a protected branch does four simple things:
See the [Changelog](#changelog) section for changes over time.
+>
+>Additional functionality for GitLab Enterprise Edition:
+>
+>- Restrict push and merge access to [certain users][ee-restrict]
+
## Configuring protected branches
To protect a branch, you need to have at least Master permission level. Note
@@ -28,22 +35,41 @@ that the `master` branch is protected by default.
1. From the **Branch** dropdown menu, select the branch you want to protect and
click **Protect**. In the screenshot below, we chose the `develop` branch.
- ![Choose protected branch](img/protected_branches_choose_branch.png)
+ ![Protected branches page](img/protected_branches_page.png)
-1. Once done, the protected branch will appear in the "Already protected" list.
+1. Once done, the protected branch will appear in the "Protected branches" list.
![Protected branches list](img/protected_branches_list.png)
+## Using the Allowed to merge and Allowed to push settings
+
+> [Introduced][ce-5081] in GitLab 8.11.
+
+Since GitLab 8.11, we added another layer of branch protection which provides
+more granular management of protected branches. The "Developers can push"
+option was replaced by an "Allowed to push" setting which can be set to
+allow/prohibit Masters and/or Developers to push to a protected branch.
+
+Using the "Allowed to push" and "Allowed to merge" settings, you can control
+the actions that different roles can perform with the protected branch.
+For example, you could set "Allowed to push" to "No one", and "Allowed to merge"
+to "Developers + Masters", to require _everyone_ to submit a merge request for
+changes going into the protected branch. This is compatible with workflows like
+the [GitLab workflow](../../workflow/gitlab_flow.md).
+
+However, there are workflows where that is not needed, and only protecting from
+force pushes and branch removal is useful. For those workflows, you can allow
+everyone with write access to push to a protected branch by setting
+"Allowed to push" to "Developers + Masters".
+
+You can set the "Allowed to push" and "Allowed to merge" options while creating
+a protected branch or afterwards by selecting the option you want from the
+dropdown list in the "Already protected" area.
-Since GitLab 8.10, we added another layer of branch protection which provides
-more granular management of protected branches. You can now choose the option
-"Developers can merge" so that Developer users can merge a merge request but
-not directly push. In that case, your branches are protected from direct pushes,
-yet Developers don't need elevated permissions or wait for someone with a higher
-permission level to press merge.
+![Developers can push](img/protected_branches_devs_can_push.png)
-You can set this option while creating the protected branch or after its
-creation.
+If you don't choose any of those options while creating a protected branch,
+they are set to "Masters" by default.
## Wildcard protected branches
@@ -66,40 +92,25 @@ Two different wildcards can potentially match the same branch. For example,
In that case, if _any_ of these protected branches have a setting like
"Allowed to push", then `production-stable` will also inherit this setting.
-If you click on a protected branch's name that is created using a wildcard,
-you will be presented with a list of all matching branches:
+If you click on a protected branch's name, you will be presented with a list of
+all matching branches:
![Protected branch matches](img/protected_branches_matches.png)
-## Restrict the creation of protected branches
-
-Creating a protected branch or a list of protected branches using the wildcard
-feature, not only you are restricting pushes to those branches, but also their
-creation if not already created.
-
-## Error messages when pushing to a protected branch
-
-A user with insufficient permissions will be presented with an error when
-creating or pushing to a branch that's prohibited, either through GitLab's UI:
-
-![Protected branch error GitLab UI](img/protected_branches_error_ui.png)
-
-or using Git from their terminal:
+## Changelog
-```bash
-remote: GitLab: You are not allowed to push code to protected branches on this project.
-To https://gitlab.example.com/thedude/bowling.git
- ! [remote rejected] staging-stable -> staging-stable (pre-receive hook declined)
-error: failed to push some refs to 'https://gitlab.example.com/thedude/bowling.git'
-```
+**8.11**
-## Changelog
+- Allow creating protected branches that can't be pushed to [gitlab-org/gitlab-ce!5081][ce-5081]
-**8.10.0**
+**8.10**
-- Allow specifying protected branches using wildcards [gitlab-org/gitlab-ce!5081][ce-4665]
+- Allow developers to merge into a protected branch without having push access [gitlab-org/gitlab-ce!4892][ce-4892]
+- Allow specifying protected branches using wildcards [gitlab-org/gitlab-ce!4665][ce-4665]
---
[ce-4665]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4665 "Allow specifying protected branches using wildcards"
+[ce-4892]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4892 "Allow developers to merge into a protected branch without having push access"
[ce-5081]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5081 "Allow creating protected branches that can't be pushed to"
+[ee-restrict]: http://docs.gitlab.com/ee/user/project/protected_branches.html#restricting-push-and-merge-access-to-certain-users
diff --git a/doc/workflow/slash_commands.md b/doc/user/project/slash_commands.md
index 91d69d4e77e..1792a0c501d 100644
--- a/doc/workflow/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -26,5 +26,5 @@ do.
| `/done` | Mark todo as done |
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
-| `/due <in 2 days | this Friday | December 31st>` | Set due date |
+| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
| `/remove_due_date` | Remove due date |
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 1653d95e722..0cf56449de2 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -6,8 +6,8 @@
- [Feature branch workflow](workflow.md)
- [GitLab Flow](gitlab_flow.md)
- [Groups](groups.md)
+- [Issue Board](../user/project/issue_board.md)
- [Keyboard shortcuts](shortcuts.md)
-- [Slash commands](slash_commands.md)
- [File finder](file_finder.md)
- [Labels](../user/project/labels.md)
- [Notification emails](notifications.md)
@@ -15,6 +15,7 @@
- [Project forking workflow](forking_workflow.md)
- [Project users](add-user/add-user.md)
- [Protected branches](../user/project/protected_branches.md)
+- [Slash commands](../user/project/slash_commands.md)
- [Sharing a project with a group](share_with_group.md)
- [Share projects with other groups](share_projects_with_other_groups.md)
- [Web Editor](web_editor.md)
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 2b2f140f8bf..7c0eb90d540 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -89,7 +89,7 @@ In this case the master branch is deployed on staging. When someone wants to dep
And going live with code happens by merging the pre-production branch into the production branch.
This workflow where commits only flow downstream ensures that everything has been tested on all environments.
If you need to cherry-pick a commit with a hotfix it is common to develop it on a feature branch and merge it into master with a merge request, do not delete the feature branch.
-If master is good to go (it should be if you a practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
+If master is good to go (it should be if you are practicing [continuous delivery](http://martinfowler.com/bliki/ContinuousDelivery.html)) you then merge it to the other branches.
If this is not possible because more manual testing is required you can send merge requests from the feature branch to the downstream branches.
An 'extreme' version of environment branches are setting up an environment for each feature branch as done by [Teatro](https://teatro.io/).
@@ -115,7 +115,7 @@ In this flow it is not common to have a production branch (or git flow master br
Merge or pull requests are created in a git management application and ask an assigned person to merge two branches.
Tools such as GitHub and Bitbucket choose the name pull request since the first manual action would be to pull the feature branch.
-Tools such as GitLab and Gitorious choose the name merge request since that is the final action that is requested of the assignee.
+Tools such as GitLab and others choose the name merge request since that is the final action that is requested of the assignee.
In this article we'll refer to them as merge requests.
If you work on a feature branch for more than a few hours it is good to share the intermediate result with the rest of the team.
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index d2ec56e6504..40a5e4476be 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -15,6 +15,25 @@ Please note that you need to have builds configured to enable this feature.
## Checkout merge requests locally
+### By adding a git alias
+
+Add the following alias to your `~/.gitconfig`:
+
+```
+[alias]
+ mr = !sh -c 'git fetch $1 merge-requests/$2/head:mr-$1-$2 && git checkout mr-$1-$2' -
+```
+
+Now you can check out a particular merge request from any repository and any remote, e.g. to check out a merge request number 5 as shown in GitLab from the `upstream` remote, do:
+
+```
+$ git mr upstream 5
+```
+
+This will fetch the merge request into a local `mr-upstream-5` branch and check it out.
+
+### By modifying `.git/config` for a given repository
+
Locate the section for your GitLab remote in the `.git/config` file. It looks like this:
```
@@ -34,7 +53,7 @@ It should look like this:
fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
```
-Now you can fetch all the merge requests requests:
+Now you can fetch all the merge requests:
```
$ git fetch origin
@@ -61,3 +80,11 @@ If you click the "Hide whitespace changes" button, you can see the diff without
It is also working on commits compare view.
![Commit Compare](merge_requests/commit_compare.png)
+
+## Merge Requests versions
+
+Every time you push to merge request branch, a new version of merge request diff
+is created. When you visit the merge request page you see latest version of changes.
+However you can select an older one from version dropdown
+
+![Merge Request Versions](merge_requests/versions.png)
diff --git a/doc/workflow/merge_requests/versions.png b/doc/workflow/merge_requests/versions.png
new file mode 100644
index 00000000000..c0a6dfe6806
--- /dev/null
+++ b/doc/workflow/merge_requests/versions.png
Binary files differ
diff --git a/doc/workflow/project_features.md b/doc/workflow/project_features.md
index a523b3facbe..f19e7df8c9a 100644
--- a/doc/workflow/project_features.md
+++ b/doc/workflow/project_features.md
@@ -32,4 +32,12 @@ Snippets are little bits of code or text.
This is a nice place to put code or text that is used semi-regularly within the project, but does not belong in source control.
-For example, a specific config file that is used by > the team that is only valid for the people that work on the code.
+For example, a specific config file that is used by the team that is only valid for the people that work on the code.
+
+## Git LFS
+
+>**Note:** Project-specific LFS setting was added on 8.12 and is available only to admins.
+
+Git Large File Storage allows you to easily manage large binary files with Git.
+With this setting admins can better control which projects are allowed to use
+LFS.
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
index 4c59f59c587..8e50cb03e63 100644
--- a/doc/workflow/share_projects_with_other_groups.md
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -1,22 +1,24 @@
# Share Projects with other Groups
-In GitLab Enterprise Edition you can share projects with other groups.
-This makes it possible to add a group of users to a project with a single action.
+You can share projects with other groups. This makes it possible to add a group of users
+to a project with a single action.
## Groups as collections of users
-In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
-In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
+Groups are used primarily to [create collections of projects](groups.md), but you can also
+take advantage of the fact that groups define collections of _users_, namely the group
+members.
## Sharing a project with a group of users
-The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
-But what if 'Project Acme' already belongs to another group, say 'Open Source'?
-This is where the (Enterprise Edition only) group sharing feature can be of use.
+The primary mechanism to give a group of users, say 'Engineering', access to a project,
+say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
+Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
-![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
+![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice.
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
diff --git a/features/project/commits/branches.feature b/features/project/commits/branches.feature
index 2c17d32154a..88fef674c0c 100644
--- a/features/project/commits/branches.feature
+++ b/features/project/commits/branches.feature
@@ -22,6 +22,7 @@ Feature: Project Commits Branches
@javascript
Scenario: I delete a branch
Given I visit project branches page
+ And I filter for branch improve/awesome
And I click branch 'improve/awesome' delete link
Then I should not see branch 'improve/awesome'
diff --git a/features/project/merge_requests.feature b/features/project/merge_requests.feature
index 6bac6011467..5aa592e9067 100644
--- a/features/project/merge_requests.feature
+++ b/features/project/merge_requests.feature
@@ -24,7 +24,7 @@ Feature: Project Merge Requests
Scenario: I should see target branch when it is different from default
Given project "Shop" have "Bug NS-06" open merge request
When I visit project "Shop" merge requests page
- Then I should see "other_branch" branch
+ Then I should see "feature_conflict" branch
Scenario: I should not see the numbers of diverged commits if the branch is rebased on the target
Given project "Shop" have "Bug NS-07" open merge request with rebased branch
@@ -89,7 +89,7 @@ Feature: Project Merge Requests
Then The list should be sorted by "Oldest updated"
@javascript
- Scenario: Visiting Merge Requests from a differente Project after sorting
+ Scenario: Visiting Merge Requests from a different Project after sorting
Given I visit project "Shop" merge requests page
And I sort the list by "Oldest updated"
And I visit dashboard merge requests page
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index f0d8d498e46..2f0941e4113 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -18,7 +18,6 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('GitHub')
expect(page).to have_link('Bitbucket')
expect(page).to have_link('GitLab.com')
- expect(page).to have_link('Gitorious.org')
expect(page).to have_link('Google Code')
expect(page).to have_link('Repo by URL')
end
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index dfa2fa75def..e9b45823c67 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -116,8 +116,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- click_button "Edit access level"
- select 'Developer', from: 'group_member_access_level'
+ click_button 'Edit'
+ select 'Developer', from: "member_access_level_#{member.id}"
click_on 'Save'
end
end
diff --git a/features/steps/project/commits/branches.rb b/features/steps/project/commits/branches.rb
index 4bfb7e92e99..5f9b9e0445e 100644
--- a/features/steps/project/commits/branches.rb
+++ b/features/steps/project/commits/branches.rb
@@ -73,6 +73,11 @@ class Spinach::Features::ProjectCommitsBranches < Spinach::FeatureSteps
expect(page).to have_content 'Branch already exists'
end
+ step 'I filter for branch improve/awesome' do
+ fill_in 'branch-search', with: 'improve/awesome'
+ find('#branch-search').native.send_keys(:enter)
+ end
+
step "I click branch 'improve/awesome' delete link" do
page.within '.js-branch-improve\/awesome' do
find('.btn-remove').click
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 9778ff4a6c7..56b28949585 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -58,8 +58,8 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
expect(find('.merge-request-info')).not_to have_content "master"
end
- step 'I should see "other_branch" branch' do
- expect(page).to have_content "other_branch"
+ step 'I should see "feature_conflict" branch' do
+ expect(page).to have_content "feature_conflict"
end
step 'I should see "Bug NS-04" in merge requests' do
@@ -124,7 +124,7 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
source_project: project,
target_project: project,
source_branch: 'fix',
- target_branch: 'other_branch',
+ target_branch: 'feature_conflict',
author: project.users.first,
description: "# Description header"
)
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 841d191d55b..bb79424ee08 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -44,7 +44,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see its content with new lines preserved at end of file' do
- expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n"
+ expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n"
end
step 'I click link "Raw"' do
@@ -65,7 +65,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I can edit code' do
set_new_content
- expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content
+ expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content
end
step 'I edit code' do
@@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I edit code with new lines at end of file' do
- execute_script('blob.editor.setValue("Sample\n\n\n")')
+ execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
end
step 'I fill the new file name' do
@@ -378,7 +378,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
private
def set_new_content
- execute_script("blob.editor.setValue('#{new_gitignore_content}')")
+ execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
end
# Content of the gitignore file on the seed repository.
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index f32576d2cb1..e920f5a706b 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,8 +65,8 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- click_button "Edit access level"
- select "Reporter", from: "project_member_access_level"
+ click_button 'Edit'
+ select "Reporter", from: "member_access_level_#{project_member.id}"
click_button "Save"
end
end
diff --git a/features/steps/shared/issuable.rb b/features/steps/shared/issuable.rb
index aa666a954bc..df9845ba569 100644
--- a/features/steps/shared/issuable.rb
+++ b/features/steps/shared/issuable.rb
@@ -179,7 +179,7 @@ module SharedIssuable
project = Project.find_by(name: from_project_name)
expect(page).to have_content(user_name)
- expect(page).to have_content("mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
+ expect(page).to have_content("Mentioned in #{issuable.class.to_s.titleize.downcase} #{issuable.to_reference(project)}")
end
def expect_sidebar_content(content)
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 6b8bfbbdae6..4602e627fdb 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -18,22 +18,14 @@ module API
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' }.to_json, 500)
+ handle_api_exception(exception)
end
format :json
content_type :txt, "text/plain"
# Ensure the namespace is right, otherwise we might load Grape::API::Helpers
+ helpers ::SentryHelper
helpers ::API::Helpers
mount ::API::AccessRequests
@@ -75,5 +67,6 @@ module API
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
+ mount ::API::MergeRequestDiffs
end
end
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 2efe7e3adf3..7c22b17e4e5 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -54,7 +54,7 @@ module API
post endpoint do
required_attributes! [:name]
- not_found!('Award Emoji') unless can_read_awardable?
+ not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user)
@@ -92,6 +92,10 @@ module API
can?(current_user, ability, awardable)
end
+ def can_award_awardable?
+ awardable.user_can_award?(current_user, params[:name])
+ end
+
def awardable
@awardable ||=
begin
diff --git a/lib/api/commit_statuses.rb b/lib/api/commit_statuses.rb
index 4df6ca8333e..5e3c9563703 100644
--- a/lib/api/commit_statuses.rb
+++ b/lib/api/commit_statuses.rb
@@ -64,7 +64,7 @@ module API
ref = branches.first
end
- pipeline = @project.ensure_pipeline(commit.sha, ref, current_user)
+ pipeline = @project.ensure_pipeline(ref, commit.sha, current_user)
name = params[:name] || params[:context]
status = GenericCommitStatus.running_or_pending.find_by(pipeline: pipeline, name: name, ref: params[:ref])
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 67420772335..4335e3055ef 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -49,7 +49,7 @@ module API
class ProjectHook < Hook
expose :project_id, :push_events
expose :issues_events, :merge_requests_events, :tag_push_events
- expose :note_events, :build_events, :pipeline_events
+ expose :note_events, :build_events, :pipeline_events, :wiki_page_events
expose :enable_ssl_verification
end
@@ -78,7 +78,7 @@ module API
expose :path, :path_with_namespace
expose :issues_enabled, :merge_requests_enabled, :wiki_enabled, :builds_enabled, :snippets_enabled, :container_registry_enabled
expose :created_at, :last_activity_at
- expose :shared_runners_enabled
+ expose :shared_runners_enabled, :lfs_enabled
expose :creator_id
expose :namespace
expose :forked_from_project, using: Entities::BasicProjectDetails, if: lambda{ |project, options| project.forked? }
@@ -90,6 +90,7 @@ module API
expose :shared_with_groups do |project, options|
SharedGroup.represent(project.project_group_links.all, options)
end
+ expose :only_allow_merge_if_build_succeeds
end
class Member < UserBasic
@@ -97,6 +98,10 @@ module API
member = options[:member] || options[:members].find { |m| m.user_id == user.id }
member.access_level
end
+ expose :expires_at do |user, options|
+ member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member.expires_at
+ end
end
class AccessRequester < UserBasic
@@ -173,6 +178,10 @@ module API
# TODO (rspeicher): Deprecated; remove in 9.0
expose(:expires_at) { |snippet| nil }
+
+ expose :web_url do |snippet, options|
+ Gitlab::UrlBuilder.build(snippet)
+ end
end
class ProjectEntity < Grape::Entity
@@ -202,6 +211,10 @@ module API
expose :user_notes_count
expose :upvotes, :downvotes
expose :due_date
+
+ expose :web_url do |issue, options|
+ Gitlab::UrlBuilder.build(issue)
+ end
end
class ExternalIssue < Grape::Entity
@@ -225,6 +238,10 @@ module API
expose :user_notes_count
expose :should_remove_source_branch?, as: :should_remove_source_branch
expose :force_remove_source_branch?, as: :force_remove_source_branch
+
+ expose :web_url do |merge_request, options|
+ Gitlab::UrlBuilder.build(merge_request)
+ end
end
class MergeRequestChanges < MergeRequest
@@ -233,6 +250,19 @@ module API
end
end
+ class MergeRequestDiff < Grape::Entity
+ expose :id, :head_commit_sha, :base_commit_sha, :start_commit_sha,
+ :created_at, :merge_request_id, :state, :real_size
+ end
+
+ class MergeRequestDiffFull < MergeRequestDiff
+ expose :commits, using: Entities::RepoCommit
+
+ expose :diffs, using: Entities::RepoDiff do |compare, _|
+ compare.raw_diffs(all_diffs: true).to_a
+ end
+ end
+
class SSHKey < Grape::Entity
expose :id, :title, :key, :created_at
end
@@ -510,13 +540,12 @@ module API
expose :duration
end
- class Environment < Grape::Entity
+ class EnvironmentBasic < Grape::Entity
expose :id, :name, :external_url
- expose :project, using: Entities::Project
end
- class EnvironmentBasic < Grape::Entity
- expose :id, :name, :external_url
+ class Environment < EnvironmentBasic
+ expose :project, using: Entities::Project
end
class Deployment < Grape::Entity
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index d0469d6602d..da4b1bf9902 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -279,6 +279,24 @@ module API
error!({ 'message' => message }, status)
end
+ def handle_api_exception(exception)
+ if sentry_enabled? && report_exception?(exception)
+ define_params_for_grape_middleware
+ sentry_context
+ Raven.capture_exception(exception)
+ end
+
+ # lifted from https://github.com/rails/rails/blob/master/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb#L60
+ 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' }.to_json, 500)
+ end
+
# Projects helpers
def filter_projects(projects)
@@ -419,5 +437,19 @@ module API
Entities::Issue
end
end
+
+ # The Grape Error Middleware only has access to env but no params. We workaround this by
+ # defining a method that returns the right value.
+ def define_params_for_grape_middleware
+ self.define_singleton_method(:params) { Rack::Request.new(env).params.symbolize_keys }
+ end
+
+ # We could get a Grape or a standard Ruby exception. We should only report anything that
+ # is clearly an error.
+ def report_exception?(exception)
+ return true unless exception.respond_to?(:status)
+
+ exception.status == 500
+ end
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index d8e9ac406c4..5b54c11ef62 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -101,6 +101,31 @@ module API
{}
end
end
+
+ post '/two_factor_recovery_codes' do
+ status 200
+
+ key = Key.find(params[:key_id])
+ user = key.user
+
+ # Make sure this isn't a deploy key
+ unless key.type.nil?
+ return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
+ end
+
+ unless user.present?
+ return { success: false, message: 'Could not find a user for the given key' }
+ end
+
+ unless user.two_factor_enabled?
+ return { success: false, message: 'Two-factor authentication is not enabled for this user' }
+ end
+
+ codes = user.generate_otp_backup_codes!
+ user.save!
+
+ { success: true, recovery_codes: codes }
+ end
end
end
end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 077258faee1..d0bc7243e54 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -154,21 +154,15 @@ module API
render_api_error!({ labels: errors }, 400)
end
- project = user_project
+ attrs[:labels] = params[:labels] if params[:labels]
- issue = ::Issues::CreateService.new(project, current_user, attrs.merge(request: request, api: true)).execute
+ issue = ::Issues::CreateService.new(user_project, current_user, attrs.merge(request: request, api: true)).execute
if issue.spam?
render_api_error!({ error: 'Spam detected' }, 400)
end
if issue.valid?
- # Find or create labels and attach to issue. Labels are valid because
- # we already checked its name, so there can't be an error here
- if params[:labels].present?
- issue.add_labels_by_names(params[:labels].split(','))
- end
-
present issue, with: Entities::Issue, current_user: current_user
else
render_validation_error!(issue)
@@ -202,17 +196,11 @@ module API
render_api_error!({ labels: errors }, 400)
end
+ attrs[:labels] = params[:labels] if params[:labels]
+
issue = ::Issues::UpdateService.new(user_project, current_user, attrs).execute(issue)
if issue.valid?
- # Find or create labels and attach to issue. Labels are valid because
- # we already checked its name, so there can't be an error here
- if params[:labels] && can?(current_user, :admin_issue, user_project)
- issue.remove_labels
- # Create and add labels to the new created issue
- issue.add_labels_by_names(params[:labels].split(','))
- end
-
present issue, with: Entities::Issue, current_user: current_user
else
render_validation_error!(issue)
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 2fae83f60b2..94c16710d9a 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -49,6 +49,7 @@ module API
# id (required) - The group/project ID
# user_id (required) - The user ID of the new member
# access_level (required) - A valid access level
+ # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
#
# Example Request:
# POST /groups/:id/members
@@ -72,7 +73,7 @@ module API
conflict!('Member already exists') if source_type == 'group' && member
unless member
- source.add_user(params[:user_id], params[:access_level], current_user)
+ source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
member = source.members.find_by(user_id: params[:user_id])
end
@@ -81,7 +82,7 @@ module API
else
# Since `source.add_user` doesn't return a member object, we have to
# build a new one and populate its errors in order to render them.
- member = source.members.build(attributes_for_keys([:user_id, :access_level]))
+ member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
member.valid? # populate the errors
# This is to ensure back-compatibility but 400 behavior should be used
@@ -97,6 +98,7 @@ module API
# id (required) - The group/project ID
# user_id (required) - The user ID of the member
# access_level (required) - A valid access level
+ # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
#
# Example Request:
# PUT /groups/:id/members/:user_id
@@ -107,8 +109,9 @@ module API
required_attributes! [:user_id, :access_level]
member = source.members.find_by!(user_id: params[:user_id])
+ attrs = attributes_for_keys [:access_level, :expires_at]
- if member.update_attributes(access_level: params[:access_level])
+ if member.update_attributes(attrs)
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
new file mode 100644
index 00000000000..07435d78468
--- /dev/null
+++ b/lib/api/merge_request_diffs.rb
@@ -0,0 +1,45 @@
+module API
+ # MergeRequestDiff API
+ class MergeRequestDiffs < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ desc 'Get a list of merge request diff versions' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::MergeRequestDiff
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions" do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+
+ authorize! :read_merge_request, merge_request
+ present merge_request.merge_request_diffs, with: Entities::MergeRequestDiff
+ end
+
+ desc 'Get a single merge request diff version' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::MergeRequestDiffFull
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :merge_request_id, type: Integer, desc: 'The ID of a merge request'
+ requires :version_id, type: Integer, desc: 'The ID of a merge request diff version'
+ end
+
+ get ":id/merge_requests/:merge_request_id/versions/:version_id" do
+ merge_request = user_project.merge_requests.
+ find(params[:merge_request_id])
+
+ authorize! :read_merge_request, merge_request
+ present merge_request.merge_request_diffs.find(params[:version_id]), with: Entities::MergeRequestDiffFull
+ end
+ end
+ end
+end
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 3f63cd678e8..14f5be3b5f6 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -46,6 +46,7 @@ module API
:note_events,
:build_events,
:pipeline_events,
+ :wiki_page_events,
:enable_ssl_verification
]
@hook = user_project.hooks.new(attrs)
@@ -80,6 +81,7 @@ module API
:note_events,
:build_events,
:pipeline_events,
+ :wiki_page_events,
:enable_ssl_verification
]
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 60cfc103afd..f8979a1cc29 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -105,6 +105,7 @@ module API
# visibility_level (optional) - 0 by default
# import_url (optional)
# public_builds (optional)
+ # lfs_enabled (optional)
# Example Request
# POST /projects
post do
@@ -123,7 +124,9 @@ module API
:public,
:visibility_level,
:import_url,
- :public_builds]
+ :public_builds,
+ :only_allow_merge_if_build_succeeds,
+ :lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(current_user, attrs).execute
if @project.saved?
@@ -155,6 +158,7 @@ module API
# visibility_level (optional)
# import_url (optional)
# public_builds (optional)
+ # lfs_enabled (optional)
# Example Request
# POST /projects/user/:user_id
post "user/:user_id" do
@@ -172,7 +176,9 @@ module API
:public,
:visibility_level,
:import_url,
- :public_builds]
+ :public_builds,
+ :only_allow_merge_if_build_succeeds,
+ :lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
@project = ::Projects::CreateService.new(user, attrs).execute
if @project.saved?
@@ -218,6 +224,7 @@ module API
# public (optional) - if true same as setting visibility_level = 20
# visibility_level (optional) - visibility level of a project
# public_builds (optional)
+ # lfs_enabled (optional)
# Example Request
# PUT /projects/:id
put ':id' do
@@ -234,7 +241,9 @@ module API
:shared_runners_enabled,
:public,
:visibility_level,
- :public_builds]
+ :public_builds,
+ :only_allow_merge_if_build_succeeds,
+ :lfs_enabled]
attrs = map_public_to_visibility_level(attrs)
authorize_admin_project
authorize! :rename_project, user_project if attrs[:name].present?
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index f117fc3d37d..9fcd9a3f999 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -55,7 +55,7 @@ module Backup
bk_repos_path = File.join(path, '..', 'repositories.old.' + Time.now.to_i.to_s)
FileUtils.mv(path, bk_repos_path)
# This is expected from gitlab:check
- FileUtils.mkdir_p(path, mode: 2770)
+ FileUtils.mkdir_p(path, mode: 02770)
end
Project.find_each(batch_size: 1000) do |project|
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 17bb99a2ae5..a6b9beecded 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -9,22 +9,14 @@ module Ci
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)
+ handle_api_exception(exception)
end
content_type :txt, 'text/plain'
content_type :json, 'application/json'
format :json
+ helpers ::SentryHelper
helpers ::Ci::API::Helpers
helpers ::API::Helpers
helpers Gitlab::CurrentSettings
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 84688f6646e..a4558d157c0 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -94,7 +94,7 @@ module ExtractsPath
@options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
@options = HashWithIndifferentAccess.new(@options)
- @id = Addressable::URI.normalize_component(get_id)
+ @id = get_id
@ref, @path = extract_ref(@id)
@repo = @project.repository
if @options[:extended_sha1].blank?
@@ -119,6 +119,7 @@ module ExtractsPath
private
+ # overriden in subclasses, do not remove
def get_id
id = params[:id] || params[:ref]
id += "/" + params[:path] unless params[:path].blank?
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
index 3d56ea3e47a..9a0482306b7 100644
--- a/lib/gitlab/badge/coverage/report.rb
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -12,10 +12,7 @@ module Gitlab
@ref = ref
@job = job
- @pipeline = @project.pipelines
- .where(ref: @ref)
- .where(sha: @project.commit(@ref).try(:sha))
- .first
+ @pipeline = @project.pipelines.latest_successful_for(@ref)
end
def entity
diff --git a/lib/gitlab/ci/config/node/hidden_job.rb b/lib/gitlab/ci/config/node/hidden.rb
index 073044b66f8..fe4ee8a7fc6 100644
--- a/lib/gitlab/ci/config/node/hidden_job.rb
+++ b/lib/gitlab/ci/config/node/hidden.rb
@@ -5,11 +5,10 @@ module Gitlab
##
# Entry that represents a hidden CI/CD job.
#
- class HiddenJob < Entry
+ class Hidden < Entry
include Validatable
validations do
- validates :config, type: Hash
validates :config, presence: true
end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
index 51683c82ceb..a1a26d4fd8f 100644
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ b/lib/gitlab/ci/config/node/jobs.rb
@@ -30,7 +30,7 @@ module Gitlab
def compose!
@config.each do |name, config|
- node = hidden?(name) ? Node::HiddenJob : Node::Job
+ node = hidden?(name) ? Node::Hidden : Node::Job
factory = Node::Factory.new(node)
.value(config || {})
diff --git a/lib/gitlab/conflict/file.rb b/lib/gitlab/conflict/file.rb
index 0a1fd27ced5..dff9e29c6a5 100644
--- a/lib/gitlab/conflict/file.rb
+++ b/lib/gitlab/conflict/file.rb
@@ -181,6 +181,17 @@ module Gitlab
sections: sections
}
end
+
+ # Don't try to print merge_request or repository.
+ def inspect
+ instance_variables = [:merge_file_result, :their_path, :our_path, :our_mode].map do |instance_variable|
+ value = instance_variable_get("@#{instance_variable}")
+
+ "#{instance_variable}=\"#{value}\""
+ end
+
+ "#<#{self.class} #{instance_variables.join(' ')}>"
+ end
end
end
end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index 6eccded7872..2d4d55daeeb 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -13,10 +13,19 @@ module Gitlab
class UnmergeableFile < ParserError
end
+ class UnsupportedEncoding < ParserError
+ end
+
def parse(text, our_path:, their_path:, parent_file: nil)
raise UnmergeableFile if text.blank? # Typically a binary file
raise UnmergeableFile if text.length > 102400
+ begin
+ text.to_json
+ rescue Encoding::UndefinedConversionError
+ raise UnsupportedEncoding
+ end
+
line_obj_index = 0
line_old = 1
line_new = 1
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 9dc2602867e..bd681f03173 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -23,7 +23,6 @@ module Gitlab
dates.each do |date|
date_id = date.to_time.to_i.to_s
- @timestamps[date_id] = 0
day_events = events.find { |day_events| day_events["date"] == date }
if day_events
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 735331df66c..12fbb78c53e 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -30,6 +30,7 @@ module Gitlab
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
+ koding_enabled: false,
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
@@ -40,7 +41,7 @@ module Gitlab
default_project_visibility: Settings.gitlab.default_projects_features['visibility_level'],
default_snippet_visibility: Settings.gitlab.default_projects_features['visibility_level'],
domain_whitelist: Settings.gitlab['domain_whitelist'],
- import_sources: %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project],
+ import_sources: %w[github bitbucket gitlab google_code fogbugz git gitlab_project],
shared_runners_enabled: Settings.gitlab_ci['shared_runners_enabled'],
max_artifacts_size: Settings.artifacts['max_size'],
require_two_factor_authentication: false,
diff --git a/lib/gitlab/diff/file_collection/merge_request.rb b/lib/gitlab/diff/file_collection/merge_request_diff.rb
index 4f946908e2f..36348b33943 100644
--- a/lib/gitlab/diff/file_collection/merge_request.rb
+++ b/lib/gitlab/diff/file_collection/merge_request_diff.rb
@@ -1,14 +1,14 @@
module Gitlab
module Diff
module FileCollection
- class MergeRequest < Base
- def initialize(merge_request, diff_options:)
- @merge_request = merge_request
+ class MergeRequestDiff < Base
+ def initialize(merge_request_diff, diff_options:)
+ @merge_request_diff = merge_request_diff
- super(merge_request,
- project: merge_request.project,
+ super(merge_request_diff,
+ project: merge_request_diff.project,
diff_options: diff_options,
- diff_refs: merge_request.diff_refs)
+ diff_refs: merge_request_diff.diff_refs)
end
def diff_files
@@ -61,11 +61,11 @@ module Gitlab
end
def cacheable?
- @merge_request.merge_request_diff.present?
+ @merge_request_diff.present?
end
def cache_key
- [@merge_request.merge_request_diff, 'highlighted-diff-files', diff_options]
+ [@merge_request_diff, 'highlighted-diff-files', diff_options]
end
end
end
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 2fdcf8d7838..ecf62dead35 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -139,13 +139,19 @@ module Gitlab
private
def find_diff_file(repository)
- diffs = Gitlab::Git::Compare.new(
- repository.raw_repository,
- start_sha,
- head_sha
- ).diffs(paths: paths)
+ # We're at the initial commit, so just get that as we can't compare to anything.
+ if Gitlab::Git.blank_ref?(start_sha)
+ compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
+ else
+ compare = Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ start_sha,
+ head_sha
+ )
+ end
+
+ diff = compare.diffs(paths: paths).first
- diff = diffs.first
return unless diff
Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs)
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index bd3267e2a80..5cf9d5ebe28 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -4,7 +4,8 @@ require 'gitlab/email/handler/create_issue_handler'
module Gitlab
module Email
module Handler
- HANDLERS = [CreateNoteHandler, CreateIssueHandler]
+ # The `CreateIssueHandler` feature is disabled for the time being.
+ HANDLERS = [CreateNoteHandler]
def self.for(mail, mail_key)
HANDLERS.find do |klass|
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 9ddc8905bd6..02ffb43d89b 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -3,12 +3,13 @@ module Gitlab
class Importer
include Gitlab::ShellAdapter
- attr_reader :client, :project, :repo, :repo_url
+ attr_reader :client, :errors, :project, :repo, :repo_url
def initialize(project)
@project = project
@repo = project.import_source
@repo_url = project.import_url
+ @errors = []
if credentials
@client = Client.new(credentials[:user])
@@ -18,8 +19,14 @@ module Gitlab
end
def execute
- import_labels && import_milestones && import_issues &&
- import_pull_requests && import_wiki
+ import_labels
+ import_milestones
+ import_issues
+ import_pull_requests
+ import_wiki
+ handle_errors
+
+ true
end
private
@@ -28,22 +35,37 @@ module Gitlab
@credentials ||= project.import_data.credentials if project.import_data
end
+ def handle_errors
+ return unless errors.any?
+
+ project.update_column(:import_error, {
+ message: 'The remote data could not be fully imported.',
+ errors: errors
+ }.to_json)
+ end
+
def import_labels
labels = client.labels(repo, per_page: 100)
- labels.each { |raw| LabelFormatter.new(project, raw).create! }
- true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
+ labels.each do |raw|
+ begin
+ LabelFormatter.new(project, raw).create!
+ rescue => e
+ errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
+ end
end
def import_milestones
milestones = client.milestones(repo, state: :all, per_page: 100)
- milestones.each { |raw| MilestoneFormatter.new(project, raw).create! }
- true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
+ milestones.each do |raw|
+ begin
+ MilestoneFormatter.new(project, raw).create!
+ rescue => e
+ errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
+ end
end
def import_issues
@@ -53,15 +75,15 @@ module Gitlab
gh_issue = IssueFormatter.new(project, raw)
if gh_issue.valid?
- issue = gh_issue.create!
- apply_labels(issue)
- import_comments(issue) if gh_issue.has_comments?
+ begin
+ issue = gh_issue.create!
+ apply_labels(issue)
+ import_comments(issue) if gh_issue.has_comments?
+ rescue => e
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
-
- true
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
end
def import_pull_requests
@@ -77,14 +99,12 @@ module Gitlab
apply_labels(merge_request)
import_comments(merge_request)
import_comments_on_diff(merge_request)
- rescue ActiveRecord::RecordInvalid => e
- raise Projects::ImportService::Error, e.message
+ rescue => e
+ errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
ensure
clean_up_restored_branches(pull_request)
end
end
-
- true
end
def restore_source_branch(pull_request)
@@ -98,7 +118,7 @@ module Gitlab
def remove_branch(name)
project.repository.delete_branch(name)
rescue Rugged::ReferenceError
- nil
+ errors << { type: :remove_branch, name: name }
end
def clean_up_restored_branches(pull_request)
@@ -112,9 +132,10 @@ module Gitlab
issue = client.issue(repo, issuable.iid)
if issue.labels.count > 0
- label_ids = issue.labels.map do |raw|
- Label.find_by(LabelFormatter.new(project, raw).attributes).try(:id)
- end
+ label_ids = issue.labels
+ .map { |raw| LabelFormatter.new(project, raw).attributes }
+ .map { |attrs| Label.find_by(attrs).try(:id) }
+ .compact
issuable.update_attribute(:label_ids, label_ids)
end
@@ -132,8 +153,12 @@ module Gitlab
def create_comments(issuable, comments)
comments.each do |raw|
- comment = CommentFormatter.new(project, raw)
- issuable.notes.create!(comment.attributes)
+ begin
+ comment = CommentFormatter.new(project, raw)
+ issuable.notes.create!(comment.attributes)
+ rescue => e
+ errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
@@ -143,16 +168,12 @@ module Gitlab
gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
project.update_attribute(:wiki_enabled, true)
end
-
- true
rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created,
# this means that repo has wiki enabled, but have no pages. So,
# we can skip the import.
if e.message !~ /repository not exported/
- raise Projects::ImportService::Error, e.message
- else
- true
+ errors << { type: :wiki, errors: e.message }
end
end
end
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index b84538a090a..04aa3664f64 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -56,6 +56,10 @@ module Gitlab
end
end
+ def url
+ raw_data.url
+ end
+
private
def assigned?
diff --git a/lib/gitlab/gitorious_import.rb b/lib/gitlab/gitorious_import.rb
deleted file mode 100644
index 8d0132a744c..00000000000
--- a/lib/gitlab/gitorious_import.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module Gitlab
- module GitoriousImport
- GITORIOUS_HOST = "https://gitorious.org"
- end
-end
diff --git a/lib/gitlab/gitorious_import/client.rb b/lib/gitlab/gitorious_import/client.rb
deleted file mode 100644
index 99fe5bdebfc..00000000000
--- a/lib/gitlab/gitorious_import/client.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module Gitlab
- module GitoriousImport
- class Client
- attr_reader :repo_list
-
- def initialize(repo_list)
- @repo_list = repo_list
- end
-
- def authorize_url(redirect_uri)
- "#{GITORIOUS_HOST}/gitlab-import?callback_url=#{redirect_uri}"
- end
-
- def repos
- @repos ||= repo_names.map { |full_name| GitoriousImport::Repository.new(full_name) }
- end
-
- def repo(id)
- repos.find { |repo| repo.id == id }
- end
-
- private
-
- def repo_names
- repo_list.to_s.split(',').map(&:strip).reject(&:blank?)
- end
- end
- end
-end
diff --git a/lib/gitlab/gitorious_import/project_creator.rb b/lib/gitlab/gitorious_import/project_creator.rb
deleted file mode 100644
index 8e22aa9286d..00000000000
--- a/lib/gitlab/gitorious_import/project_creator.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-module Gitlab
- module GitoriousImport
- class ProjectCreator
- attr_reader :repo, :namespace, :current_user
-
- def initialize(repo, namespace, current_user)
- @repo = repo
- @namespace = namespace
- @current_user = current_user
- end
-
- def execute
- ::Projects::CreateService.new(
- current_user,
- name: repo.name,
- path: repo.path,
- description: repo.description,
- namespace_id: namespace.id,
- visibility_level: Gitlab::VisibilityLevel::PUBLIC,
- import_type: "gitorious",
- import_source: repo.full_name,
- import_url: repo.import_url
- ).execute
- end
- end
- end
-end
diff --git a/lib/gitlab/gitorious_import/repository.rb b/lib/gitlab/gitorious_import/repository.rb
deleted file mode 100644
index c88f1ae358d..00000000000
--- a/lib/gitlab/gitorious_import/repository.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-module Gitlab
- module GitoriousImport
- Repository = Struct.new(:full_name) do
- def id
- Digest::SHA1.hexdigest(full_name)
- end
-
- def namespace
- segments.first
- end
-
- def path
- segments.last
- end
-
- def name
- path.titleize
- end
-
- def description
- ""
- end
-
- def import_url
- "#{GITORIOUS_HOST}/#{full_name}.git"
- end
-
- private
-
- def segments
- full_name.split('/')
- end
- end
- end
-end
diff --git a/lib/gitlab/import_sources.rb b/lib/gitlab/import_sources.rb
index 59a05411fe9..94261b7eeed 100644
--- a/lib/gitlab/import_sources.rb
+++ b/lib/gitlab/import_sources.rb
@@ -14,13 +14,12 @@ module Gitlab
def options
{
- 'GitHub' => 'github',
- 'Bitbucket' => 'bitbucket',
- 'GitLab.com' => 'gitlab',
- 'Gitorious.org' => 'gitorious',
- 'Google Code' => 'google_code',
- 'FogBugz' => 'fogbugz',
- 'Repo by URL' => 'git',
+ 'GitHub' => 'github',
+ 'Bitbucket' => 'bitbucket',
+ 'GitLab.com' => 'gitlab',
+ 'Google Code' => 'google_code',
+ 'FogBugz' => 'fogbugz',
+ 'Repo by URL' => 'git',
'GitLab export' => 'gitlab_project'
}
end
diff --git a/lib/gitlab/metrics/rack_middleware.rb b/lib/gitlab/metrics/rack_middleware.rb
index b4493bf44d2..01c96a6fe96 100644
--- a/lib/gitlab/metrics/rack_middleware.rb
+++ b/lib/gitlab/metrics/rack_middleware.rb
@@ -4,6 +4,17 @@ module Gitlab
class RackMiddleware
CONTROLLER_KEY = 'action_controller.instance'
ENDPOINT_KEY = 'api.endpoint'
+ CONTENT_TYPES = {
+ 'text/html' => :html,
+ 'text/plain' => :txt,
+ 'application/json' => :json,
+ 'text/js' => :js,
+ 'application/atom+xml' => :atom,
+ 'image/png' => :png,
+ 'image/jpeg' => :jpeg,
+ 'image/gif' => :gif,
+ 'image/svg+xml' => :svg
+ }
def initialize(app)
@app = app
@@ -46,8 +57,15 @@ module Gitlab
end
def tag_controller(trans, env)
- controller = env[CONTROLLER_KEY]
- trans.action = "#{controller.class.name}##{controller.action_name}"
+ controller = env[CONTROLLER_KEY]
+ action = "#{controller.class.name}##{controller.action_name}"
+ suffix = CONTENT_TYPES[controller.content_type]
+
+ if suffix && suffix != :html
+ action += ".#{suffix}"
+ end
+
+ trans.action = action
end
def tag_endpoint(trans, env)
diff --git a/lib/gitlab/middleware/rails_queue_duration.rb b/lib/gitlab/middleware/rails_queue_duration.rb
index 56608b1b276..5d2d7d0026c 100644
--- a/lib/gitlab/middleware/rails_queue_duration.rb
+++ b/lib/gitlab/middleware/rails_queue_duration.rb
@@ -11,7 +11,7 @@ module Gitlab
def call(env)
trans = Gitlab::Metrics.current_transaction
- proxy_start = env['HTTP_GITLAB_WORHORSE_PROXY_START'].presence
+ proxy_start = env['HTTP_GITLAB_WORKHORSE_PROXY_START'].presence
if trans && proxy_start
# Time in milliseconds since gitlab-workhorse started the request
trans.set(:rails_queue_duration, Time.now.to_f * 1_000 - proxy_start.to_f / 1_000_000)
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index fe65c246101..99d0c28e749 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -22,6 +22,8 @@ module Gitlab
note_url
when WikiPage
wiki_page_url
+ when ProjectSnippet
+ project_snippet_url(object)
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index d13fe0ef8a9..e59ead5d76c 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -7,7 +7,7 @@ module Gitlab
# @param cmd [Array<String>]
# @return [Boolean]
def system_silent(cmd)
- Popen::popen(cmd).last.zero?
+ Popen.popen(cmd).last.zero?
end
def force_utf8(str)
diff --git a/spec/controllers/admin/impersonations_controller_spec.rb b/spec/controllers/admin/impersonations_controller_spec.rb
index d5f0b289b5b..8be662974a0 100644
--- a/spec/controllers/admin/impersonations_controller_spec.rb
+++ b/spec/controllers/admin/impersonations_controller_spec.rb
@@ -77,6 +77,8 @@ describe Admin::ImpersonationsController do
context "when the impersonator is not blocked" do
it "redirects to the impersonated user's page" do
+ expect(Gitlab::AppLogger).to receive(:info).with("User #{impersonator.username} has stopped impersonating #{user.username}").and_call_original
+
delete :destroy
expect(response).to redirect_to(admin_user_path(user))
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 44128a43362..a121cb2fc97 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -237,6 +237,56 @@ describe AutocompleteController do
end
end
+ context 'authorized projects apply limit' do
+ before do
+ authorized_project2 = create(:project)
+ authorized_project3 = create(:project)
+
+ authorized_project.team << [user, :master]
+ authorized_project2.team << [user, :master]
+ authorized_project3.team << [user, :master]
+
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+ end
+
+ describe 'GET #projects with project ID' do
+ before do
+ get(:projects, project_id: project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 3 # Of a total of 4
+ end
+ end
+ end
+
+ context 'authorized projects with offset' do
+ before do
+ authorized_project2 = create(:project)
+ authorized_project3 = create(:project)
+
+ authorized_project.team << [user, :master]
+ authorized_project2.team << [user, :master]
+ authorized_project3.team << [user, :master]
+ end
+
+ describe 'GET #projects with project ID and offset_id' do
+ before do
+ get(:projects, project_id: project.id, offset_id: authorized_project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
+ expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either
+ end
+ end
+ end
+
context 'authorized projects without admin_issue ability' do
before(:each) do
authorized_project.team << [user, :guest]
diff --git a/spec/controllers/import/gitorious_controller_spec.rb b/spec/controllers/import/gitorious_controller_spec.rb
deleted file mode 100644
index 4ae2b78e11c..00000000000
--- a/spec/controllers/import/gitorious_controller_spec.rb
+++ /dev/null
@@ -1,69 +0,0 @@
-require 'spec_helper'
-
-describe Import::GitoriousController do
- include ImportSpecHelper
-
- let(:user) { create(:user) }
-
- before do
- sign_in(user)
- end
-
- describe "GET new" do
- it "redirects to import endpoint on gitorious.org" do
- get :new
-
- expect(controller).to redirect_to("https://gitorious.org/gitlab-import?callback_url=http://test.host/import/gitorious/callback")
- end
- end
-
- describe "GET callback" do
- it "stores repo list in session" do
- get :callback, repos: 'foo/bar,baz/qux'
-
- expect(session[:gitorious_repos]).to eq('foo/bar,baz/qux')
- end
- end
-
- describe "GET status" do
- before do
- @repo = OpenStruct.new(full_name: 'asd/vim')
- end
-
- it "assigns variables" do
- @project = create(:project, import_type: 'gitorious', 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: 'gitorious', creator_id: user.id, import_source: 'asd/vim')
- stub_client(repos: [@repo])
-
- get :status
-
- expect(assigns(:already_added_projects)).to eq([@project])
- expect(assigns(:repos)).to eq([])
- end
- end
-
- describe "POST create" do
- before do
- @repo = Gitlab::GitoriousImport::Repository.new('asd/vim')
- end
-
- it "takes already existing namespace" do
- namespace = create(:namespace, name: "asd", owner: user)
- expect(Gitlab::GitoriousImport::ProjectCreator).
- to receive(:new).with(@repo, namespace, user).
- and_return(double(execute: true))
- stub_client(repo: @repo)
-
- post :create, format: :js
- end
- end
-end
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index 9496636e3cc..261f35f28ed 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -39,7 +39,7 @@ describe Projects::Boards::ListsController do
allow(Ability.abilities).to receive(:allowed?).with(user, :read_list, project).and_return(false)
end
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
read_board_list user: user
expect(response).to have_http_status(403)
@@ -56,9 +56,9 @@ describe Projects::Boards::ListsController do
end
describe 'POST create' do
- let(:label) { create(:label, project: project, name: 'Development') }
-
context 'with valid params' do
+ let(:label) { create(:label, project: project, name: 'Development') }
+
it 'returns a successful 200 response' do
create_board_list user: user, label_id: label.id
@@ -73,20 +73,29 @@ describe Projects::Boards::ListsController do
end
context 'with invalid params' do
- it 'returns an error' do
- create_board_list user: user, label_id: nil
+ context 'when label is nil' do
+ it 'returns a not found 404 response' do
+ create_board_list user: user, label_id: nil
+
+ expect(response).to have_http_status(404)
+ end
+ end
- parsed_response = JSON.parse(response.body)
+ context 'when label that does not belongs to project' do
+ it 'returns a not found 404 response' do
+ label = create(:label, name: 'Development')
- expect(parsed_response['label']).to contain_exactly "can't be blank"
- expect(response).to have_http_status(422)
+ create_board_list user: user, label_id: label.id
+
+ expect(response).to have_http_status(404)
+ end
end
end
context 'with unauthorized user' do
- let(:label) { create(:label, project: project, name: 'Development') }
+ it 'returns a forbidden 403 response' do
+ label = create(:label, project: project, name: 'Development')
- it 'returns a successful 403 response' do
create_board_list user: guest, label_id: label.id
expect(response).to have_http_status(403)
@@ -122,7 +131,7 @@ describe Projects::Boards::ListsController do
end
context 'with invalid position' do
- it 'returns a unprocessable entity 422 response' do
+ it 'returns an unprocessable entity 422 response' do
move user: user, list: planning, position: 6
expect(response).to have_http_status(422)
@@ -138,7 +147,7 @@ describe Projects::Boards::ListsController do
end
context 'with unauthorized user' do
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
move user: guest, list: planning, position: 6
expect(response).to have_http_status(403)
@@ -180,7 +189,7 @@ describe Projects::Boards::ListsController do
end
context 'with unauthorized user' do
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
remove_board_list user: guest, list: planning
expect(response).to have_http_status(403)
@@ -213,7 +222,7 @@ describe Projects::Boards::ListsController do
end
context 'when board lists is not empty' do
- it 'returns a unprocessable entity 422 response' do
+ it 'returns an unprocessable entity 422 response' do
create(:list, board: board)
generate_default_board_lists user: user
@@ -223,7 +232,7 @@ describe Projects::Boards::ListsController do
end
context 'with unauthorized user' do
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
generate_default_board_lists user: guest
expect(response).to have_http_status(403)
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 0836b71056c..16929767ddf 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -8,13 +8,13 @@ describe Projects::IssuesController do
describe "GET #index" do
context 'external issue tracker' do
it 'redirects to the external issue tracker' do
- external = double(issues_url: 'https://example.com/issues')
+ external = double(project_path: 'https://example.com/project')
allow(project).to receive(:external_issue_tracker).and_return(external)
controller.instance_variable_set(:@project, project)
get :index, namespace_id: project.namespace.path, project_id: project
- expect(response).to redirect_to('https://example.com/issues')
+ expect(response).to redirect_to('https://example.com/project')
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c64c2b075c5..a219400d75f 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -170,6 +170,35 @@ describe Projects::MergeRequestsController do
expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
expect(merge_request.reload.closed?).to be_truthy
end
+
+ it 'allows editing of a closed merge request' do
+ merge_request.close!
+
+ put :update,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: merge_request.iid,
+ merge_request: {
+ title: 'New title'
+ }
+
+ expect(response).to redirect_to([merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request])
+ expect(merge_request.reload.title).to eq 'New title'
+ end
+
+ it 'does not allow to update target branch closed merge request' do
+ merge_request.close!
+
+ put :update,
+ namespace_id: project.namespace.path,
+ project_id: project.path,
+ id: merge_request.iid,
+ merge_request: {
+ target_branch: 'new_branch'
+ }
+
+ expect { merge_request.reload.target_branch }.not_to change { merge_request.target_branch }
+ end
end
end
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 4fd51a23490..424ecc65759 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -14,6 +14,7 @@ FactoryGirl.define do
note_events true
build_events true
pipeline_events true
+ wiki_page_events true
end
end
end
diff --git a/spec/features/admin/admin_system_info_spec.rb b/spec/features/admin/admin_system_info_spec.rb
index f4e5c26b519..1df972843e2 100644
--- a/spec/features/admin/admin_system_info_spec.rb
+++ b/spec/features/admin/admin_system_info_spec.rb
@@ -6,12 +6,49 @@ describe 'Admin System Info' do
end
describe 'GET /admin/system_info' do
- it 'shows system info page' do
- visit admin_system_info_path
+ let(:cpu) { double(:cpu, length: 2) }
+ let(:memory) { double(:memory, active_bytes: 4294967296, total_bytes: 17179869184) }
- expect(page).to have_content 'CPU'
- expect(page).to have_content 'Memory'
- expect(page).to have_content 'Disks'
+ context 'when all info is available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_return(cpu)
+ allow(Vmstat).to receive(:memory).and_return(memory)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page' do
+ expect(page).to have_content 'CPU 2 cores'
+ expect(page).to have_content 'Memory 4 GB / 16 GB'
+ expect(page).to have_content 'Disks'
+ end
+ end
+
+ context 'when CPU info is not available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_raise(Errno::ENOENT)
+ allow(Vmstat).to receive(:memory).and_return(memory)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page with no CPU info' do
+ expect(page).to have_content 'CPU Unable to collect CPU info'
+ expect(page).to have_content 'Memory 4 GB / 16 GB'
+ expect(page).to have_content 'Disks'
+ end
+ end
+
+ context 'when memory info is not available' do
+ before do
+ allow(Vmstat).to receive(:cpu).and_return(cpu)
+ allow(Vmstat).to receive(:memory).and_raise(Errno::ENOENT)
+ visit admin_system_info_path
+ end
+
+ it 'shows system info page with no CPU info' do
+ expect(page).to have_content 'CPU 2 cores'
+ expect(page).to have_content 'Memory Unable to collect memory info'
+ expect(page).to have_content 'Disks'
+ end
end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 8910c50c294..55e5dc15428 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -110,6 +110,45 @@ describe 'Issue Boards', feature: true, js: true do
end
end
+ it 'search backlog list' do
+ page.within('#js-boards-seach') do
+ find('.form-control').set(issue1.title)
+ end
+
+ wait_for_vue_resource
+
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
+ end
+
+ it 'search done list' do
+ page.within('#js-boards-seach') do
+ find('.form-control').set(issue8.title)
+ end
+
+ wait_for_vue_resource
+
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 1)
+ end
+
+ it 'search list' do
+ page.within('#js-boards-seach') do
+ find('.form-control').set(issue5.title)
+ end
+
+ wait_for_vue_resource
+
+ expect(find('.board:nth-child(1)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(2)')).to have_selector('.card', count: 1)
+ expect(find('.board:nth-child(3)')).to have_selector('.card', count: 0)
+ expect(find('.board:nth-child(4)')).to have_selector('.card', count: 0)
+ end
+
it 'allows user to delete board' do
page.within(find('.board:nth-child(2)')) do
find('.board-delete').click
@@ -162,32 +201,6 @@ describe 'Issue Boards', feature: true, js: true do
end
end
- it 'is searchable' do
- page.within(find('.board', match: :first)) do
- find('.form-control').set issue1.title
-
- wait_for_vue_resource(spinner: false)
-
- expect(page).to have_selector('.card', count: 1)
- end
- end
-
- it 'clears search' do
- page.within(find('.board', match: :first)) do
- find('.form-control').set issue1.title
-
- expect(page).to have_selector('.card', count: 1)
-
- find('.board-search-clear-btn').click
- end
-
- wait_for_vue_resource
-
- page.within(find('.board', match: :first)) do
- expect(page).to have_selector('.card', count: 6)
- end
- end
-
it 'moves issue from backlog into list' do
drag_to(list_to_index: 1)
@@ -572,6 +585,18 @@ describe 'Issue Boards', feature: true, js: true do
end
end
+ context 'keyboard shortcuts' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'allows user to use keyboard shortcuts' do
+ find('.boards-list').native.send_keys('i')
+ expect(page).to have_content('New Issue')
+ end
+ end
+
context 'signed out user' do
before do
logout
diff --git a/spec/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
index 6eb04cf74c5..79cc50bc18e 100644
--- a/spec/features/issues/award_emoji_spec.rb
+++ b/spec/features/issues/award_emoji_spec.rb
@@ -12,7 +12,6 @@ describe 'Awards Emoji', feature: true do
describe 'Click award emoji from issue#show' do
let!(:issue) do
create(:issue,
- author: @user,
assignee: @user,
project: project)
end
diff --git a/spec/features/issues/new_branch_button_spec.rb b/spec/features/issues/new_branch_button_spec.rb
index e528aff4d41..fb0c4704285 100644
--- a/spec/features/issues/new_branch_button_spec.rb
+++ b/spec/features/issues/new_branch_button_spec.rb
@@ -20,7 +20,7 @@ feature 'Start new branch from an issue', feature: true do
context "when there is a referenced merge request" do
let(:note) do
create(:note, :on_issue, :system, project: project,
- note: "mentioned in !#{referenced_mr.iid}")
+ note: "Mentioned in !#{referenced_mr.iid}")
end
let(:referenced_mr) do
create(:merge_request, :simple, source_project: project, target_project: project,
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index cb445e22af0..d744d0eb6af 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -122,6 +122,17 @@ describe 'Issues', feature: true do
expect(page).to have_content date.to_s(:medium)
end
end
+
+ it 'warns about version conflict' do
+ issue.update(title: "New title")
+
+ fill_in 'issue_title', with: 'bug 345'
+ fill_in 'issue_description', with: 'bug description'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Someone edited the issue the same time you did'
+ end
end
end
@@ -525,7 +536,7 @@ describe 'Issues', feature: true do
end
end
- describe 'new issue by email' do
+ xdescribe 'new issue by email' do
shared_examples 'show the email in the modal' do
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
diff --git a/spec/features/merge_requests/conflicts_spec.rb b/spec/features/merge_requests/conflicts_spec.rb
index 930c36ade2b..759edf8ec80 100644
--- a/spec/features/merge_requests/conflicts_spec.rb
+++ b/spec/features/merge_requests/conflicts_spec.rb
@@ -43,7 +43,8 @@ feature 'Merge request conflict resolution', js: true, feature: true do
'conflict-too-large' => 'when the conflicts contain a large file',
'conflict-binary-file' => 'when the conflicts contain a binary file',
'conflict-contains-conflict-markers' => 'when the conflicts contain a file with ambiguous conflict markers',
- 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another'
+ 'conflict-missing-side' => 'when the conflicts contain a file edited in one branch and deleted in another',
+ 'conflict-non-utf8' => 'when the conflicts contain a non-UTF-8 file',
}
UNRESOLVABLE_CONFLICTS.each do |source_branch, description|
diff --git a/spec/features/merge_requests/diff_notes_spec.rb b/spec/features/merge_requests/diff_notes_spec.rb
new file mode 100644
index 00000000000..06fad1007e8
--- /dev/null
+++ b/spec/features/merge_requests/diff_notes_spec.rb
@@ -0,0 +1,238 @@
+require 'spec_helper'
+
+feature 'Diff notes', js: true, feature: true do
+ include WaitForAjax
+
+ before do
+ login_as :admin
+ @merge_request = create(:merge_request)
+ @project = @merge_request.source_project
+ end
+
+ context 'merge request diffs' do
+ let(:comment_button_class) { '.add-diff-note' }
+ let(:notes_holder_input_class) { 'js-temp-notes-holder' }
+ let(:notes_holder_input_xpath) { './following-sibling::*[contains(concat(" ", @class, " "), " notes_holder ")]' }
+ let(:test_note_comment) { 'this is a test note!' }
+
+ context 'when hovering over a parallel view diff file' do
+ before(:each) do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'parallel')
+ end
+
+ context 'with an old line on the left and no line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_23_22"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with no line on the left and a new line on the right' do
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_15_15"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an old line on the left and a new line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_9_9"]').find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an unchanged line on the left and an unchanged line on the right' do
+ it 'should allow commenting on the left side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'left')
+ end
+
+ it 'should allow commenting on the right side' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]', match: :first).find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with a match line' do
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(find('.match', match: :first).find(:xpath, '..'), 'right')
+ end
+ end
+
+ context 'with an unfolded line' do
+ before(:each) do
+ find('.js-unfold', match: :first).click
+ wait_for_ajax
+ end
+
+ # The first `.js-unfold` unfolds upwards, therefore the first
+ # `.line_holder` will be an unfolded line.
+ let(:line_holder) { first('.line_holder[id="1"]') }
+
+ it 'should not allow commenting on the left side' do
+ should_not_allow_commenting(line_holder, 'left')
+ end
+
+ it 'should not allow commenting on the right side' do
+ should_not_allow_commenting(line_holder, 'right')
+ end
+ end
+ end
+
+ context 'when hovering over an inline view diff file' do
+ before do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+ end
+
+ context 'with a new line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+
+ context 'with an old line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ end
+ end
+
+ context 'with an unchanged line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ end
+ end
+
+ context 'with a match line' do
+ it 'should not allow commenting' do
+ should_not_allow_commenting(find('.match', match: :first))
+ end
+ end
+
+ context 'with an unfolded line' do
+ before(:each) do
+ find('.js-unfold', match: :first).click
+ wait_for_ajax
+ end
+
+ # The first `.js-unfold` unfolds upwards, therefore the first
+ # `.line_holder` will be an unfolded line.
+ let(:line_holder) { first('.line_holder[id="1"]') }
+
+ it 'should not allow commenting' do
+ should_not_allow_commenting line_holder
+ end
+ end
+
+ context 'when hovering over a diff discussion' do
+ before do
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ visit namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ end
+
+ it 'should not allow commenting' do
+ should_not_allow_commenting(find('.line_holder', match: :first))
+ end
+ end
+ end
+
+ context 'when the MR only supports legacy diff notes' do
+ before do
+ @merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
+ visit diffs_namespace_project_merge_request_path(@project.namespace, @project, @merge_request, view: 'inline')
+ end
+
+ context 'with a new line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_10_9"]'))
+ end
+ end
+
+ context 'with an old line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="6eb14e00385d2fb284765eb1cd8d420d33d63fc9_22_22"]'))
+ end
+ end
+
+ context 'with an unchanged line' do
+ it 'should allow commenting' do
+ should_allow_commenting(find('[id="2f6fcd96b88b36ce98c38da085c795a27d92a3dd_7_7"]'))
+ end
+ end
+
+ context 'with a match line' do
+ it 'should not allow commenting' do
+ should_not_allow_commenting(find('.match', match: :first))
+ end
+ end
+ end
+
+ def should_allow_commenting(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ expect(line[:num]).to have_css comment_button_class
+
+ comment_on_line(line_holder, line)
+
+ assert_comment_persistence(line_holder)
+ end
+
+ def should_not_allow_commenting(line_holder, diff_side = nil)
+ line = get_line_components(line_holder, diff_side)
+ line[:content].hover
+ expect(line[:num]).not_to have_css comment_button_class
+ end
+
+ def get_line_components(line_holder, diff_side = nil)
+ if diff_side.nil?
+ get_inline_line_components(line_holder)
+ else
+ get_parallel_line_components(line_holder, diff_side)
+ end
+ end
+
+ def get_inline_line_components(line_holder)
+ { content: line_holder.find('.line_content', match: :first), num: line_holder.find('.diff-line-num', match: :first) }
+ end
+
+ def get_parallel_line_components(line_holder, diff_side = nil)
+ side_index = diff_side == 'left' ? 0 : 1
+ # Wait for `.line_content`
+ line_holder.find('.line_content', match: :first)
+ # Wait for `.diff-line-num`
+ line_holder.find('.diff-line-num', match: :first)
+ { content: line_holder.all('.line_content')[side_index], num: line_holder.all('.diff-line-num')[side_index] }
+ end
+
+ def comment_on_line(line_holder, line)
+ line[:num].find(comment_button_class).trigger 'click'
+ line_holder.find(:xpath, notes_holder_input_xpath)
+
+ notes_holder_input = line_holder.find(:xpath, notes_holder_input_xpath)
+ expect(notes_holder_input[:class]).to include(notes_holder_input_class)
+
+ notes_holder_input.fill_in 'note[note]', with: test_note_comment
+ click_button 'Comment'
+ wait_for_ajax
+ end
+
+ def assert_comment_persistence(line_holder)
+ expect(line_holder).to have_xpath notes_holder_input_xpath
+
+ notes_holder_saved = line_holder.find(:xpath, notes_holder_input_xpath)
+ expect(notes_holder_saved[:class]).not_to include(notes_holder_input_class)
+ expect(notes_holder_saved).to have_content test_note_comment
+ end
+ end
+end
diff --git a/spec/features/merge_requests/edit_mr_spec.rb b/spec/features/merge_requests/edit_mr_spec.rb
index 4109e78f560..c77e719c5df 100644
--- a/spec/features/merge_requests/edit_mr_spec.rb
+++ b/spec/features/merge_requests/edit_mr_spec.rb
@@ -17,5 +17,16 @@ feature 'Edit Merge Request', feature: true do
it 'has class js-quick-submit in form' do
expect(page).to have_selector('.js-quick-submit')
end
+
+ it 'warns about version conflict' do
+ merge_request.update(title: "New title")
+
+ fill_in 'merge_request_title', with: 'bug 345'
+ fill_in 'merge_request_description', with: 'bug description'
+
+ click_button 'Save changes'
+
+ expect(page).to have_content 'Someone edited the merge request the same time you did'
+ end
end
end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
new file mode 100644
index 00000000000..577c910f11b
--- /dev/null
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+feature 'Merge Request versions', js: true, feature: true do
+ before do
+ login_as :admin
+ merge_request = create(:merge_request, importing: true)
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project = merge_request.source_project
+ visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'show the latest version of the diff' do
+ page.within '.mr-version-switch' do
+ expect(page).to have_content 'Version: latest'
+ end
+
+ expect(page).to have_content '8 changed files'
+ end
+
+ describe 'switch between versions' do
+ before do
+ page.within '.mr-version-switch' do
+ find('.btn-link').click
+ click_link '6f6d7e7e'
+ end
+ end
+
+ it 'should show older version' do
+ page.within '.mr-version-switch' do
+ expect(page).to have_content 'Version: 6f6d7e7e'
+ end
+
+ expect(page).to have_content '5 changed files'
+ end
+ end
+end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index 7a9edbbe339..f1c522155d3 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -141,7 +141,7 @@ describe 'Comments', feature: true do
let(:project2) { create(:project, :private) }
let(:issue) { create(:issue, project: project2) }
let(:merge_request) { create(:merge_request, source_project: project, source_branch: 'markdown') }
- let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "mentioned in #{issue.to_reference(project)}") }
+ let!(:note) { create(:note_on_merge_request, :system, noteable: merge_request, project: project, note: "Mentioned in #{issue.to_reference(project)}") }
it 'shows the system note' do
login_as :admin
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index af86d3c338a..5972e7f31c2 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -4,12 +4,6 @@ feature 'test coverage badge' do
given!(:user) { create(:user) }
given!(:project) { create(:project, :private) }
- given!(:pipeline) do
- create(:ci_pipeline, project: project,
- ref: 'master',
- sha: project.commit.id)
- end
-
context 'when user has access to view badge' do
background do
project.team << [user, :developer]
@@ -17,8 +11,10 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge image for pipeline' do
- create_job(coverage: 100, name: 'test:1')
- create_job(coverage: 90, name: 'test:2')
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: 100, name: 'test:1')
+ create_build(pipeline, coverage: 90, name: 'test:2')
+ end
show_test_coverage_badge
@@ -26,9 +22,11 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge for specific job' do
- create_job(coverage: 50, name: 'test:1')
- create_job(coverage: 50, name: 'test:2')
- create_job(coverage: 85, name: 'coverage')
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: 50, name: 'test:1')
+ create_build(pipeline, coverage: 50, name: 'test:2')
+ create_build(pipeline, coverage: 85, name: 'coverage')
+ end
show_test_coverage_badge(job: 'coverage')
@@ -36,7 +34,9 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge for pipeline without coverage' do
- create_job(coverage: nil, name: 'test')
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: nil, name: 'test')
+ end
show_test_coverage_badge
@@ -54,10 +54,19 @@ feature 'test coverage badge' do
end
end
- def create_job(coverage:, name:)
- create(:ci_build, name: name,
- coverage: coverage,
- pipeline: pipeline)
+ def create_pipeline
+ opts = { project: project, ref: 'master', sha: project.commit.id }
+
+ create(:ci_pipeline, opts).tap do |pipeline|
+ yield pipeline
+ pipeline.build_updated
+ end
+ end
+
+ def create_build(pipeline, coverage:, name:)
+ opts = { pipeline: pipeline, coverage: coverage, name: name }
+
+ create(:ci_build, :success, opts)
end
def show_test_coverage_badge(job: nil)
diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb
new file mode 100644
index 00000000000..63878c55421
--- /dev/null
+++ b/spec/features/projects/branches/delete_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+feature 'Delete branch', feature: true, js: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_branches_path(project.namespace, project)
+ end
+
+ it 'destroys tooltip' do
+ first('.remove-row').hover
+ expect(page).to have_selector('.tooltip')
+
+ first('.remove-row').click
+ wait_for_ajax
+
+ expect(page).not_to have_selector('.tooltip')
+ end
+end
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
new file mode 100644
index 00000000000..04058300570
--- /dev/null
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Download buttons in branches page', feature: true do
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:status) { 'success' }
+ given(:project) { create(:project) }
+
+ given(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit('binary-encoding').sha,
+ ref: 'binary-encoding', # make sure the branch is in the 1st page!
+ status: status)
+ end
+
+ given!(:build) do
+ create(:ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build')
+ end
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when checking branches' do
+ context 'with artifacts' do
+ before do
+ visit namespace_project_branches_path(project.namespace, project)
+ end
+
+ scenario 'shows download artifacts button' do
+ expect(page).to have_link "Download '#{build.name}'"
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 79abba21854..1b14945bf0a 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -20,7 +20,7 @@ describe 'Branches', feature: true do
describe 'Find branches' do
it 'shows filtered branches', js: true do
- visit namespace_project_branches_path(project.namespace, project, project.id)
+ visit namespace_project_branches_path(project.namespace, project)
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
diff --git a/spec/features/builds_spec.rb b/spec/features/projects/builds_spec.rb
index 0cfeb2e57d8..0cfeb2e57d8 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index 1b4ff6b6f1b..e45e3a36d01 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+include WaitForAjax
describe 'Cherry-pick Commits' do
let(:project) { create(:project) }
@@ -8,12 +9,11 @@ describe 'Cherry-pick Commits' do
before do
login_as :user
project.team << [@user, :master]
- visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 })
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
end
context "I cherry-pick a commit" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
expect(page).not_to have_content('v1.0.0') # Only branches, not tags
page.within('#modal-cherry-pick-commit') do
@@ -26,7 +26,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a merge commit" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
@@ -38,7 +37,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit that was previously cherry-picked" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
@@ -56,7 +54,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit in a new merge request" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
click_button 'Cherry-pick'
@@ -64,4 +61,28 @@ describe 'Cherry-pick Commits' do
expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.')
end
end
+
+ context "I cherry-pick a commit from a different branch", js: true do
+ it do
+ find('.commit-action-buttons a.dropdown-toggle').click
+ find(:css, "a[href='#modal-cherry-pick-commit']").click
+
+ page.within('#modal-cherry-pick-commit') do
+ click_button 'master'
+ end
+
+ wait_for_ajax
+
+ page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
+ click_link 'feature'
+ end
+
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
end
diff --git a/spec/features/projects/files/download_buttons_spec.rb b/spec/features/projects/files/download_buttons_spec.rb
new file mode 100644
index 00000000000..be5cebcd7c9
--- /dev/null
+++ b/spec/features/projects/files/download_buttons_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'Download buttons in files tree', feature: true do
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:status) { 'success' }
+ given(:project) { create(:project) }
+
+ given(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: status)
+ end
+
+ given!(:build) do
+ create(:ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build')
+ end
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when files tree' do
+ context 'with artifacts' do
+ before do
+ visit namespace_project_tree_path(
+ project.namespace, project, project.default_branch)
+ end
+
+ scenario 'shows download artifacts button' do
+ expect(page).to have_link "Download '#{build.name}'"
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
new file mode 100644
index 00000000000..1a71a03fbd9
--- /dev/null
+++ b/spec/features/projects/group_links_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+feature 'Project group links', feature: true, js: true do
+ include Select2Helper
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:group) { create(:group) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ context 'setting an expiration date for a group link' do
+ before do
+ visit namespace_project_group_links_path(project.namespace, project)
+
+ select2 group.id, from: '#link_group_id'
+ fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
+ page.find('body').click
+ click_on 'Share'
+ end
+
+ it 'shows the expiration time with a warning class' do
+ page.within('.enabled-groups') do
+ expect(page).to have_content('expires in 4 days')
+ expect(page).to have_selector('.text-warning')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/main/download_buttons_spec.rb b/spec/features/projects/main/download_buttons_spec.rb
new file mode 100644
index 00000000000..b26c0ea7a14
--- /dev/null
+++ b/spec/features/projects/main/download_buttons_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+feature 'Download buttons in project main page', feature: true do
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:status) { 'success' }
+ given(:project) { create(:project) }
+
+ given(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: status)
+ end
+
+ given!(:build) do
+ create(:ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build')
+ end
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when checking project main page' do
+ context 'with artifacts' do
+ before do
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'shows download artifacts button' do
+ expect(page).to have_link "Download '#{build.name}'"
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
new file mode 100644
index 00000000000..430c384ac2e
--- /dev/null
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
+ include Select2Helper
+ include ActiveSupport::Testing::TimeHelpers
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:new_member) { create(:user) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ scenario 'expiration date is displayed in the members list' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.users-project-form' do
+ select2(new_member.id, from: '#user_ids', multiple: true)
+ fill_in 'expires_at', with: '2016-08-10'
+ click_on 'Add users to project'
+ end
+
+ page.within '.project_member:first-child' do
+ expect(page).to have_content('Expires in 4 days')
+ end
+ end
+ end
+
+ scenario 'change expiration date' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.project_member:first-child' do
+ click_on 'Edit'
+ fill_in 'Access expiration date', with: '2016-08-09'
+ click_on 'Save'
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/tags/download_buttons_spec.rb b/spec/features/projects/tags/download_buttons_spec.rb
new file mode 100644
index 00000000000..6e0022c179f
--- /dev/null
+++ b/spec/features/projects/tags/download_buttons_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'Download buttons in tags page', feature: true do
+ given(:user) { create(:user) }
+ given(:role) { :developer }
+ given(:status) { 'success' }
+ given(:tag) { 'v1.0.0' }
+ given(:project) { create(:project) }
+
+ given(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit(tag).sha,
+ ref: tag,
+ status: status)
+ end
+
+ given!(:build) do
+ create(:ci_build, :success, :artifacts,
+ pipeline: pipeline,
+ status: pipeline.status,
+ name: 'build')
+ end
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ describe 'when checking tags' do
+ context 'with artifacts' do
+ before do
+ visit namespace_project_tags_path(project.namespace, project)
+ end
+
+ scenario 'shows download artifacts button' do
+ expect(page).to have_link "Download '#{build.name}'"
+ end
+ end
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 1b14c66fe28..e00d85904d5 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -115,6 +115,35 @@ feature 'Project', feature: true do
end
end
+ describe 'tree view (default view is set to Files)' do
+ let(:user) { create(:user, project_view: 'files') }
+ let(:project) { create(:forked_project_with_submodules) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ it 'has working links to files' do
+ click_link('PROCESS.md')
+
+ expect(page.status_code).to eq(200)
+ end
+
+ it 'has working links to directories' do
+ click_link('encoding')
+
+ expect(page.status_code).to eq(200)
+ end
+
+ it 'has working links to submodules' do
+ click_link('645f6c4c')
+
+ expect(page.status_code).to eq(200)
+ end
+ end
+
def remove_with_confirm(button_text, confirm_with)
click_button button_text
fill_in 'confirm_name_input', with: confirm_with
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
new file mode 100644
index 00000000000..395c61a4743
--- /dev/null
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -0,0 +1,71 @@
+RSpec.shared_examples "protected branches > access control > CE" do
+ ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected branches that #{access_type_name} can push to" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ within('.new_protected_branch') do
+ allowed_to_push_button = find(".js-allowed-to-push")
+
+ unless allowed_to_push_button.text == access_type_name
+ allowed_to_push_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
+ end
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected branches so that #{access_type_name} can push to them" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+
+ within(".protected-branches-list") do
+ find(".js-allowed-to-push").click
+ within('.js-allowed-to-push-container') { click_on access_type_name }
+ end
+
+ wait_for_ajax
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+
+ ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected branches that #{access_type_name} can merge to" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ within('.new_protected_branch') do
+ allowed_to_merge_button = find(".js-allowed-to-merge")
+
+ unless allowed_to_merge_button.text == access_type_name
+ allowed_to_merge_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
+ end
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected branches so that #{access_type_name} can merge to them" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+
+ within(".protected-branches-list") do
+ find(".js-allowed-to-merge").click
+ within('.js-allowed-to-merge-container') { click_on access_type_name }
+ end
+
+ wait_for_ajax
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index a0ee6cab7ec..1a3f7b970f6 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f }
feature 'Projected Branches', feature: true, js: true do
include WaitForAjax
@@ -88,74 +89,6 @@ feature 'Projected Branches', feature: true, js: true do
end
describe "access control" do
- ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
- it "allows creating protected branches that #{access_type_name} can push to" do
- visit namespace_project_protected_branches_path(project.namespace, project)
- set_protected_branch_name('master')
- within('.new_protected_branch') do
- allowed_to_push_button = find(".js-allowed-to-push")
-
- unless allowed_to_push_button.text == access_type_name
- allowed_to_push_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
- end
- end
- click_on "Protect"
-
- expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
- end
-
- it "allows updating protected branches so that #{access_type_name} can push to them" do
- visit namespace_project_protected_branches_path(project.namespace, project)
- set_protected_branch_name('master')
- click_on "Protect"
-
- expect(ProtectedBranch.count).to eq(1)
-
- within(".protected-branches-list") do
- find(".js-allowed-to-push").click
- within('.js-allowed-to-push-container') { click_on access_type_name }
- end
-
- wait_for_ajax
- expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
- end
- end
-
- ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
- it "allows creating protected branches that #{access_type_name} can merge to" do
- visit namespace_project_protected_branches_path(project.namespace, project)
- set_protected_branch_name('master')
- within('.new_protected_branch') do
- allowed_to_merge_button = find(".js-allowed-to-merge")
-
- unless allowed_to_merge_button.text == access_type_name
- allowed_to_merge_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
- end
- end
- click_on "Protect"
-
- expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
- end
-
- it "allows updating protected branches so that #{access_type_name} can merge to them" do
- visit namespace_project_protected_branches_path(project.namespace, project)
- set_protected_branch_name('master')
- click_on "Protect"
-
- expect(ProtectedBranch.count).to eq(1)
-
- within(".protected-branches-list") do
- find(".js-allowed-to-merge").click
- within('.js-allowed-to-merge-container') { click_on access_type_name }
- end
-
- wait_for_ajax
- expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
- end
- end
+ include_examples "protected branches > access control > CE"
end
end
diff --git a/spec/features/search_spec.rb b/spec/features/search_spec.rb
index b7a25d80fec..dcd3a2f17b0 100644
--- a/spec/features/search_spec.rb
+++ b/spec/features/search_spec.rb
@@ -71,6 +71,16 @@ describe "Search", feature: true do
end
describe 'Right header search field', feature: true do
+ it 'allows enter key to search', js: true do
+ visit namespace_project_path(project.namespace, project)
+ fill_in 'search', with: 'gitlab'
+ find('#search').native.send_keys(:enter)
+
+ page.within '.title' do
+ expect(page).to have_content 'Search'
+ end
+ end
+
describe 'Search in project page' do
before do
visit namespace_project_path(project.namespace, project)
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 788581a26cb..40f773956d1 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -43,6 +43,20 @@ describe "Dashboard access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
+ describe "GET /koding" do
+ subject { koding_path }
+
+ context 'with Koding enabled' do
+ before do
+ stub_application_setting(koding_enabled?: true)
+ end
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :visitor }
+ end
+ end
+
describe "GET /projects/new" do
it { expect(new_project_path).to be_allowed_for :admin }
it { expect(new_project_path).to be_allowed_for :user }
diff --git a/spec/features/task_lists_spec.rb b/spec/features/task_lists_spec.rb
index 6ed279ef9be..abb27c90e0a 100644
--- a/spec/features/task_lists_spec.rb
+++ b/spec/features/task_lists_spec.rb
@@ -20,6 +20,22 @@ feature 'Task Lists', feature: true do
MARKDOWN
end
+ let(:singleIncompleteMarkdown) do
+ <<-MARKDOWN.strip_heredoc
+ This is a task list:
+
+ - [ ] Incomplete entry 1
+ MARKDOWN
+ end
+
+ let(:singleCompleteMarkdown) do
+ <<-MARKDOWN.strip_heredoc
+ This is a task list:
+
+ - [x] Incomplete entry 1
+ MARKDOWN
+ end
+
before do
Warden.test_mode!
@@ -34,77 +50,145 @@ feature 'Task Lists', feature: true do
end
describe 'for Issues' do
- let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
+ describe 'multiple tasks' do
+ let!(:issue) { create(:issue, description: markdown, author: user, project: project) }
- it 'renders' do
- visit_issue(project, issue)
+ it 'renders' do
+ visit_issue(project, issue)
- expect(page).to have_selector('ul.task-list', count: 1)
- expect(page).to have_selector('li.task-list-item', count: 6)
- expect(page).to have_selector('ul input[checked]', count: 2)
- end
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 6)
+ expect(page).to have_selector('ul input[checked]', count: 2)
+ end
+
+ it 'contains the required selectors' do
+ visit_issue(project, issue)
+
+ container = '.detail-page-description .description.js-task-list-container'
- it 'contains the required selectors' do
- visit_issue(project, issue)
+ expect(page).to have_selector(container)
+ expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector("#{container} .js-task-list-field")
+ expect(page).to have_selector('form.js-issuable-update')
+ expect(page).to have_selector('a.btn-close')
+ end
- container = '.detail-page-description .description.js-task-list-container'
+ it 'is only editable by author' do
+ visit_issue(project, issue)
+ expect(page).to have_selector('.js-task-list-container')
- expect(page).to have_selector(container)
- expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
- expect(page).to have_selector("#{container} .js-task-list-field")
- expect(page).to have_selector('form.js-issuable-update')
- expect(page).to have_selector('a.btn-close')
+ logout(:user)
+
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
+
+ it 'provides a summary on Issues#index' do
+ visit namespace_project_issues_path(project.namespace, project)
+ expect(page).to have_content("2 of 6 tasks completed")
+ end
end
- it 'is only editable by author' do
- visit_issue(project, issue)
- expect(page).to have_selector('.js-task-list-container')
+ describe 'single incomplete task' do
+ let!(:issue) { create(:issue, description: singleIncompleteMarkdown, author: user, project: project) }
- logout(:user)
+ it 'renders' do
+ visit_issue(project, issue)
- login_as(user2)
- visit current_path
- expect(page).not_to have_selector('.js-task-list-container')
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 0)
+ end
+
+ it 'provides a summary on Issues#index' do
+ visit namespace_project_issues_path(project.namespace, project)
+ expect(page).to have_content("0 of 1 task completed")
+ end
end
- it 'provides a summary on Issues#index' do
- visit namespace_project_issues_path(project.namespace, project)
- expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
+ describe 'single complete task' do
+ let!(:issue) { create(:issue, description: singleCompleteMarkdown, author: user, project: project) }
+
+ it 'renders' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+ end
+
+ it 'provides a summary on Issues#index' do
+ visit namespace_project_issues_path(project.namespace, project)
+ expect(page).to have_content("1 of 1 task completed")
+ end
end
end
describe 'for Notes' do
let!(:issue) { create(:issue, author: user, project: project) }
- let!(:note) do
- create(:note, note: markdown, noteable: issue,
- project: project, author: user)
+ describe 'multiple tasks' do
+ let!(:note) do
+ create(:note, note: markdown, noteable: issue,
+ project: project, author: user)
+ end
+
+ it 'renders for note body' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('.note ul.task-list', count: 1)
+ expect(page).to have_selector('.note li.task-list-item', count: 6)
+ expect(page).to have_selector('.note ul input[checked]', count: 2)
+ end
+
+ it 'contains the required selectors' do
+ visit_issue(project, issue)
+
+ expect(page).to have_selector('.note .js-task-list-container')
+ expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
+ expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
+ end
+
+ it 'is only editable by author' do
+ visit_issue(project, issue)
+ expect(page).to have_selector('.js-task-list-container')
+
+ logout(:user)
+
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
end
- it 'renders for note body' do
- visit_issue(project, issue)
-
- expect(page).to have_selector('.note ul.task-list', count: 1)
- expect(page).to have_selector('.note li.task-list-item', count: 6)
- expect(page).to have_selector('.note ul input[checked]', count: 2)
- end
+ describe 'single incomplete task' do
+ let!(:note) do
+ create(:note, note: singleIncompleteMarkdown, noteable: issue,
+ project: project, author: user)
+ end
- it 'contains the required selectors' do
- visit_issue(project, issue)
+ it 'renders for note body' do
+ visit_issue(project, issue)
- expect(page).to have_selector('.note .js-task-list-container')
- expect(page).to have_selector('.note .js-task-list-container .task-list .task-list-item .task-list-item-checkbox')
- expect(page).to have_selector('.note .js-task-list-container .js-task-list-field')
+ expect(page).to have_selector('.note ul.task-list', count: 1)
+ expect(page).to have_selector('.note li.task-list-item', count: 1)
+ expect(page).to have_selector('.note ul input[checked]', count: 0)
+ end
end
- it 'is only editable by author' do
- visit_issue(project, issue)
- expect(page).to have_selector('.js-task-list-container')
+ describe 'single complete task' do
+ let!(:note) do
+ create(:note, note: singleCompleteMarkdown, noteable: issue,
+ project: project, author: user)
+ end
- logout(:user)
+ it 'renders for note body' do
+ visit_issue(project, issue)
- login_as(user2)
- visit current_path
- expect(page).not_to have_selector('.js-task-list-container')
+ expect(page).to have_selector('.note ul.task-list', count: 1)
+ expect(page).to have_selector('.note li.task-list-item', count: 1)
+ expect(page).to have_selector('.note ul input[checked]', count: 1)
+ end
end
end
@@ -113,42 +197,78 @@ feature 'Task Lists', feature: true do
visit namespace_project_merge_request_path(project.namespace, project, merge)
end
- let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
+ describe 'multiple tasks' do
+ let!(:merge) { create(:merge_request, :simple, description: markdown, author: user, source_project: project) }
- it 'renders for description' do
- visit_merge_request(project, merge)
+ it 'renders for description' do
+ visit_merge_request(project, merge)
- expect(page).to have_selector('ul.task-list', count: 1)
- expect(page).to have_selector('li.task-list-item', count: 6)
- expect(page).to have_selector('ul input[checked]', count: 2)
- end
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 6)
+ expect(page).to have_selector('ul input[checked]', count: 2)
+ end
- it 'contains the required selectors' do
- visit_merge_request(project, merge)
+ it 'contains the required selectors' do
+ visit_merge_request(project, merge)
- container = '.detail-page-description .description.js-task-list-container'
+ container = '.detail-page-description .description.js-task-list-container'
- expect(page).to have_selector(container)
- expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
- expect(page).to have_selector("#{container} .js-task-list-field")
- expect(page).to have_selector('form.js-issuable-update')
- expect(page).to have_selector('a.btn-close')
- end
+ expect(page).to have_selector(container)
+ expect(page).to have_selector("#{container} .wiki .task-list .task-list-item .task-list-item-checkbox")
+ expect(page).to have_selector("#{container} .js-task-list-field")
+ expect(page).to have_selector('form.js-issuable-update')
+ expect(page).to have_selector('a.btn-close')
+ end
- it 'is only editable by author' do
- visit_merge_request(project, merge)
- expect(page).to have_selector('.js-task-list-container')
+ it 'is only editable by author' do
+ visit_merge_request(project, merge)
+ expect(page).to have_selector('.js-task-list-container')
- logout(:user)
+ logout(:user)
- login_as(user2)
- visit current_path
- expect(page).not_to have_selector('.js-task-list-container')
+ login_as(user2)
+ visit current_path
+ expect(page).not_to have_selector('.js-task-list-container')
+ end
+
+ it 'provides a summary on MergeRequests#index' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ expect(page).to have_content("2 of 6 tasks completed")
+ end
+ end
+
+ describe 'single incomplete task' do
+ let!(:merge) { create(:merge_request, :simple, description: singleIncompleteMarkdown, author: user, source_project: project) }
+
+ it 'renders for description' do
+ visit_merge_request(project, merge)
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 0)
+ end
+
+ it 'provides a summary on MergeRequests#index' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ expect(page).to have_content("0 of 1 task completed")
+ end
end
- it 'provides a summary on MergeRequests#index' do
- visit namespace_project_merge_requests_path(project.namespace, project)
- expect(page).to have_content("6 tasks (2 completed, 4 remaining)")
+ describe 'single complete task' do
+ let!(:merge) { create(:merge_request, :simple, description: singleCompleteMarkdown, author: user, source_project: project) }
+
+ it 'renders for description' do
+ visit_merge_request(project, merge)
+
+ expect(page).to have_selector('ul.task-list', count: 1)
+ expect(page).to have_selector('li.task-list-item', count: 1)
+ expect(page).to have_selector('ul input[checked]', count: 1)
+ end
+
+ it 'provides a summary on MergeRequests#index' do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ expect(page).to have_content("1 of 1 task completed")
+ end
end
end
end
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
new file mode 100644
index 00000000000..e74a51acede
--- /dev/null
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe "Dashboard > User sorts todos", feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
+ let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
+
+ let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+ let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
+ let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
+
+ let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
+
+ before do
+ create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
+ create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
+ create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
+ create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
+ create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ merge_request_1.labels << label_1
+ issue_3.labels << label_1
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ project.team << [user, :developer]
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "sorts with oldest created todos first" do
+ click_link "Last created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("merge_request_1")
+ expect(results_list.all('p')[1]).to have_content("issue_1")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+
+ it "sorts with newest created todos first" do
+ click_link "Oldest created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_4")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_1")
+ expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ end
+
+ it "sorts by priority" do
+ click_link "Priority"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_3")
+ expect(results_list.all('p')[1]).to have_content("merge_request_1")
+ expect(results_list.all('p')[2]).to have_content("issue_1")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index 0342f4f1d97..32544f3f538 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -41,6 +41,27 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_content("You're all done!")
end
end
+
+ context 'todo is stale on the page' do
+ before do
+ todos = TodosFinder.new(user, state: :pending).execute
+ TodoService.new.mark_todos_as_done(todos, user)
+ end
+
+ describe 'deleting the todo' do
+ before do
+ first('.done-todo').click
+ end
+
+ it 'is removed from the list' do
+ expect(page).not_to have_selector('.todos-list .todo')
+ end
+
+ it 'shows "All done" message' do
+ expect(page).to have_content("You're all done!")
+ end
+ end
+ end
end
context 'User has Todos with labels spanning multiple projects' do
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
index 4f3304f7b6d..fdce4e714ff 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -51,6 +51,28 @@ describe MoveToProjectFinder do
expect(subject.execute(project).to_a).to eq([other_reporter_project])
end
+
+ it 'returns a page of projects ordered by id in descending order' do
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project).to_a).to eq([master_project, developer_project])
+ end
+
+ it 'returns projects after the given offset id' do
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty
+ end
end
context 'search' do
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
new file mode 100644
index 00000000000..f7e7e733cf7
--- /dev/null
+++ b/spec/finders/todos_finder_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe TodosFinder do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:finder) { described_class }
+
+ before { project.team << [user, :developer] }
+
+ describe '#sort' do
+ context 'by date' do
+ let!(:todo1) { create(:todo, user: user, project: project) }
+ let!(:todo2) { create(:todo, user: user, project: project) }
+ let!(:todo3) { create(:todo, user: user, project: project) }
+
+ it 'sorts with oldest created first' do
+ todos = finder.new(user, { sort: 'id_asc' }).execute
+
+ expect(todos.first).to eq(todo1)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo3)
+ end
+
+ it 'sorts with newest created first' do
+ todos = finder.new(user, { sort: 'id_desc' }).execute
+
+ expect(todos.first).to eq(todo3)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo1)
+ end
+ end
+
+ it "sorts by priority" do
+ label_1 = create(:label, title: 'label_1', project: project, priority: 1)
+ label_2 = create(:label, title: 'label_2', project: project, priority: 2)
+ label_3 = create(:label, title: 'label_3', project: project, priority: 3)
+
+ issue_1 = create(:issue, title: 'issue_1', project: project)
+ issue_2 = create(:issue, title: 'issue_2', project: project)
+ issue_3 = create(:issue, title: 'issue_3', project: project)
+ issue_4 = create(:issue, title: 'issue_4', project: project)
+ merge_request_1 = create(:merge_request, source_project: project)
+
+ merge_request_1.labels << label_1
+
+ # Covers the case where Todo has more than one label
+ issue_3.labels << label_1
+ issue_3.labels << label_3
+
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ todo_1 = create(:todo, user: user, project: project, target: issue_4)
+ todo_2 = create(:todo, user: user, project: project, target: issue_2)
+ todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago)
+ todo_4 = create(:todo, user: user, project: project, target: issue_1)
+ todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ todos = finder.new(user, { sort: 'priority' }).execute
+
+ expect(todos.first).to eq(todo_3)
+ expect(todos.second).to eq(todo_5)
+ expect(todos.third).to eq(todo_4)
+ expect(todos.fourth).to eq(todo_2)
+ expect(todos.fifth).to eq(todo_1)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 299e4675d6f..532ebb9640e 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -10,23 +10,31 @@
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"labels": {
- "type": ["array"],
- "required": [
- "id",
- "color",
- "description",
- "title",
- "priority"
- ],
- "properties": {
- "id": { "type": "integer" },
- "color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
},
- "description": { "type": ["string", "null"] },
- "title": { "type": "string" },
- "priority": { "type": ["integer", "null"] }
+ "additionalProperties": false
}
},
"assignee": {
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
new file mode 100644
index 00000000000..2dd2eab0524
--- /dev/null
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe IssuablesHelper do
+ let(:label) { build_stubbed(:label) }
+ let(:label2) { build_stubbed(:label) }
+
+ context 'label tooltip' do
+ it 'returns label text' do
+ expect(issuable_labels_tooltip([label])).to eq(label.title)
+ end
+
+ it 'returns label text' do
+ expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
+ end
+ end
+end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 604204cca0a..284b58d8d5c 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -136,4 +136,42 @@ describe ProjectsHelper do
expect(sanitize_repo_path(project, import_error)).to eq('Could not clone [REPOS PATH]/namespace/test.git')
end
end
+
+ describe '#last_push_event' do
+ let(:user) { double(:user, fork_of: nil) }
+ let(:project) { double(:project, id: 1) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ helper.instance_variable_set(:@project, project)
+ end
+
+ context 'when there is no current_user' do
+ let(:user) { nil }
+
+ it 'returns nil' do
+ expect(helper.last_push_event).to eq(nil)
+ end
+ end
+
+ it 'returns recent push on the current project' do
+ event = double(:event)
+ expect(user).to receive(:recent_push).with([project.id]).and_return(event)
+
+ expect(helper.last_push_event).to eq(event)
+ end
+
+ context 'when current user has a fork of the current project' do
+ let(:fork) { double(:fork, id: 2) }
+
+ it 'returns recent push considering fork events' do
+ expect(user).to receive(:fork_of).with(project).and_return(fork)
+
+ event_on_fork = double(:event)
+ expect(user).to receive(:recent_push).with([project.id, fork.id]).and_return(event_on_fork)
+
+ expect(helper.last_push_event).to eq(event_on_fork)
+ end
+ end
+ end
end
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index bf3ed5c094c..21f35585367 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -19,16 +19,16 @@ describe TimeHelper do
describe "#duration_in_numbers" do
it "returns minutes and seconds" do
- duration_in_numbers = {
- [100, 0] => "01:40",
- [121, 0] => "02:01",
- [3721, 0] => "01:02:01",
- [0, 0] => "00:00",
- [nil, Time.now.to_i - 42] => "00:42"
+ durations_and_expectations = {
+ 100 => "01:40",
+ 121 => "02:01",
+ 3721 => "01:02:01",
+ 0 => "00:00",
+ 42 => "00:42"
}
- duration_in_numbers.each do |interval, expectation|
- expect(duration_in_numbers(*interval)).to eq(expectation)
+ durations_and_expectations.each do |duration, expectation|
+ expect(duration_in_numbers(duration)).to eq(expectation)
end
end
end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index fa32d0d7da5..c1c12b57b53 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -11,7 +11,7 @@
/*= require ./fixtures/emoji_menu */
(function() {
- var awardsHandler, lazyAssert;
+ var awardsHandler, lazyAssert, urlRoot;
awardsHandler = null;
@@ -27,6 +27,7 @@
};
gon.award_menu_url = '/emojis';
+ urlRoot = gon.relative_url_root;
lazyAssert = function(done, assertFn) {
return setTimeout(function() {
@@ -45,9 +46,14 @@
return cb();
};
})(this));
- return spyOn(jQuery, 'get').and.callFake(function(req, cb) {
+ spyOn(jQuery, 'get').and.callFake(function(req, cb) {
return cb(window.emojiMenu);
});
+ spyOn(jQuery, 'cookie');
+ });
+ afterEach(function() {
+ // restore original url root value
+ gon.relative_url_root = urlRoot;
});
describe('::showEmojiMenu', function() {
it('should show emoji menu when Add emoji button clicked', function(done) {
@@ -189,6 +195,28 @@
return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
});
});
+ describe('::addEmojiToFrequentlyUsedList', function() {
+ it('should set a cookie with the correct default path', function() {
+ gon.relative_url_root = '';
+ awardsHandler.addEmojiToFrequentlyUsedList('sunglasses');
+ expect(jQuery.cookie)
+ .toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', {
+ path: '/',
+ expires: 365
+ })
+ ;
+ });
+ it('should set a cookie with the correct custom root path', function() {
+ gon.relative_url_root = '/gitlab/subdir';
+ awardsHandler.addEmojiToFrequentlyUsedList('alien');
+ expect(jQuery.cookie)
+ .toHaveBeenCalledWith('frequently_used_emojis', 'alien', {
+ path: '/gitlab/subdir',
+ expires: 365
+ })
+ ;
+ });
+ });
describe('search', function() {
return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click();
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index c206b794442..1688b996162 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -60,15 +60,6 @@ describe('List model', () => {
}, 0);
});
- it('can\'t search when not backlog', () => {
- expect(list.canSearch()).toBe(false);
- });
-
- it('can search when backlog', () => {
- list.type = 'backlog';
- expect(list.canSearch()).toBe(true);
- });
-
it('gets issue from list', (done) => {
setTimeout(() => {
const issue = list.findIssue(1);
diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee
index 6b9617341fe..8bd113e7d86 100644
--- a/spec/javascripts/datetime_utility_spec.js.coffee
+++ b/spec/javascripts/datetime_utility_spec.js.coffee
@@ -29,3 +29,22 @@ describe 'Date time utils', ->
it 'should return Saturday', ->
day = gl.utils.getDayName(new Date('07/23/2016'))
expect(day).toBe('Saturday')
+
+ describe 'get day difference', ->
+ it 'should return 7', ->
+ firstDay = new Date('07/01/2016')
+ secondDay = new Date('07/08/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(7)
+
+ it 'should return 31', ->
+ firstDay = new Date('07/01/2016')
+ secondDay = new Date('08/01/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(31)
+
+ it 'should return 365', ->
+ firstDay = new Date('07/02/2015')
+ secondDay = new Date('07/01/2016')
+ difference = gl.utils.getDayDifference(firstDay, secondDay)
+ expect(difference).toBe(365) \ No newline at end of file
diff --git a/spec/javascripts/fixtures/awards_handler.html.haml b/spec/javascripts/fixtures/awards_handler.html.haml
index d55936ee4f9..1ef2e8f8624 100644
--- a/spec/javascripts/fixtures/awards_handler.html.haml
+++ b/spec/javascripts/fixtures/awards_handler.html.haml
@@ -39,7 +39,7 @@
%span.note-role Reporter
%a.note-action-button.note-emoji-button.js-add-award.js-note-emoji{"data-position" => "right", :href => "#", :title => "Award Emoji"}
%i.fa.fa-spinner.fa-spin
- %i.fa.fa-smile-o
+ %i.fa.fa-smile-o.link-highlight
.js-task-list-container.note-body.is-task-list-enabled
.note-text
%p Suscipit sunt quia quisquam sed eveniet ipsam.
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
new file mode 100644
index 00000000000..a20390c08ee
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -0,0 +1,16 @@
+%div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
+ %i.fa.fa-times.dropdown-menu-close-icon
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
+ %i.fa.fa-search.dropdown-input-search
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
new file mode 100644
index 00000000000..397bdc85c67
--- /dev/null
+++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
@@ -0,0 +1,16 @@
+.block.labels
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip
+ .title.hide-collapsed
+ %a.edit-link.pull-right{ href: "#" }
+ Edit
+ .selectbox.hide-collapsed{ style: "display: none;" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } }
+ %span.dropdown-toggle-text
+ Label
+ %i.fa.fa-chevron-down
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/projects.json b/spec/javascripts/fixtures/projects.json
index 84e8d0ba1e4..4919d77e5a4 100644
--- a/spec/javascripts/fixtures/projects.json
+++ b/spec/javascripts/fixtures/projects.json
@@ -1 +1 @@
-[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
+[{"id":9,"description":"","default_branch":null,"tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:root/test.git","http_url_to_repo":"http://localhost:3000/root/test.git","web_url":"http://localhost:3000/root/test","owner":{"name":"Administrator","username":"root","id":1,"state":"active","avatar_url":"http://www.gravatar.com/avatar/e64c7d89f26bd1972efa854d13d7dd61?s=80\u0026d=identicon","web_url":"http://localhost:3000/u/root"},"name":"test","name_with_namespace":"Administrator / test","path":"test","path_with_namespace":"root/test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-14T19:08:05.364Z","last_activity_at":"2016-01-14T19:08:07.418Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":1,"name":"root","path":"root","owner_id":1,"created_at":"2016-01-13T20:19:44.439Z","updated_at":"2016-01-13T20:19:44.439Z","description":"","avatar":null},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":0,"permissions":{"project_access":null,"group_access":null}},{"id":8,"description":"Voluptatem quae nulla eius numquam ullam voluptatibus quia modi.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:h5bp/html5-boilerplate.git","http_url_to_repo":"http://localhost:3000/h5bp/html5-boilerplate.git","web_url":"http://localhost:3000/h5bp/html5-boilerplate","name":"Html5 Boilerplate","name_with_namespace":"H5bp / Html5 Boilerplate","path":"html5-boilerplate","path_with_namespace":"h5bp/html5-boilerplate","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:57.525Z","last_activity_at":"2016-01-13T20:27:57.280Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":5,"name":"H5bp","path":"h5bp","owner_id":null,"created_at":"2016-01-13T20:19:57.239Z","updated_at":"2016-01-13T20:19:57.239Z","description":"Tempore accusantium possimus aut libero.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":10,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":7,"description":"Modi odio mollitia dolorem qui.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:twitter/typeahead-js.git","http_url_to_repo":"http://localhost:3000/twitter/typeahead-js.git","web_url":"http://localhost:3000/twitter/typeahead-js","name":"Typeahead.Js","name_with_namespace":"Twitter / Typeahead.Js","path":"typeahead-js","path_with_namespace":"twitter/typeahead-js","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:56.212Z","last_activity_at":"2016-01-13T20:27:51.496Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":6,"description":"Omnis asperiores ipsa et beatae quidem necessitatibus quia.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:twitter/flight.git","http_url_to_repo":"http://localhost:3000/twitter/flight.git","web_url":"http://localhost:3000/twitter/flight","name":"Flight","name_with_namespace":"Twitter / Flight","path":"flight","path_with_namespace":"twitter/flight","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:54.754Z","last_activity_at":"2016-01-13T20:27:50.502Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":4,"name":"Twitter","path":"twitter","owner_id":null,"created_at":"2016-01-13T20:19:54.480Z","updated_at":"2016-01-13T20:19:54.480Z","description":"Id voluptatem ipsa maiores omnis repudiandae et et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":true,"open_issues_count":4,"permissions":{"project_access":null,"group_access":{"access_level":10,"notification_level":3}}},{"id":5,"description":"Voluptatem commodi voluptate placeat architecto beatae illum dolores fugiat.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-test.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-test.git","web_url":"http://localhost:3000/gitlab-org/gitlab-test","name":"Gitlab Test","name_with_namespace":"Gitlab Org / Gitlab Test","path":"gitlab-test","path_with_namespace":"gitlab-org/gitlab-test","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:53.202Z","last_activity_at":"2016-01-13T20:27:41.626Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":4,"description":"Aut molestias quas est ut aperiam officia quod libero.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-shell.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-shell.git","web_url":"http://localhost:3000/gitlab-org/gitlab-shell","name":"Gitlab Shell","name_with_namespace":"Gitlab Org / Gitlab Shell","path":"gitlab-shell","path_with_namespace":"gitlab-org/gitlab-shell","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:51.882Z","last_activity_at":"2016-01-13T20:27:35.678Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":20,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":3,"description":"Excepturi molestiae quia repellendus omnis est illo illum eligendi.","default_branch":"master","tag_list":[],"public":true,"archived":false,"visibility_level":20,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ci.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ci.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ci","name":"Gitlab Ci","name_with_namespace":"Gitlab Org / Gitlab Ci","path":"gitlab-ci","path_with_namespace":"gitlab-org/gitlab-ci","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:50.346Z","last_activity_at":"2016-01-13T20:27:30.115Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":3,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}},{"id":2,"description":"Adipisci quaerat dignissimos enim sed ipsam dolorem quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":10,"ssh_url_to_repo":"phil@localhost:gitlab-org/gitlab-ce.git","http_url_to_repo":"http://localhost:3000/gitlab-org/gitlab-ce.git","web_url":"http://localhost:3000/gitlab-org/gitlab-ce","name":"Gitlab Ce","name_with_namespace":"Gitlab Org / Gitlab Ce","path":"gitlab-ce","path_with_namespace":"gitlab-org/gitlab-ce","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:49.065Z","last_activity_at":"2016-01-13T20:26:58.454Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":3,"name":"Gitlab Org","path":"gitlab-org","owner_id":null,"created_at":"2016-01-13T20:19:48.851Z","updated_at":"2016-01-13T20:19:48.851Z","description":"Magni mollitia quod quidem soluta nesciunt impedit.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":{"access_level":30,"notification_level":3},"group_access":{"access_level":50,"notification_level":3}}},{"id":1,"description":"Vel voluptatem maxime saepe ex quia.","default_branch":"master","tag_list":[],"public":false,"archived":false,"visibility_level":0,"ssh_url_to_repo":"phil@localhost:documentcloud/underscore.git","http_url_to_repo":"http://localhost:3000/documentcloud/underscore.git","web_url":"http://localhost:3000/documentcloud/underscore","name":"Underscore","name_with_namespace":"Documentcloud / Underscore","path":"underscore","path_with_namespace":"documentcloud/underscore","issues_enabled":true,"merge_requests_enabled":true,"wiki_enabled":true,"builds_enabled":true,"snippets_enabled":false,"created_at":"2016-01-13T20:19:45.862Z","last_activity_at":"2016-01-13T20:25:03.106Z","shared_runners_enabled":true,"creator_id":1,"namespace":{"id":2,"name":"Documentcloud","path":"documentcloud","owner_id":null,"created_at":"2016-01-13T20:19:44.464Z","updated_at":"2016-01-13T20:19:44.464Z","description":"Aut impedit perferendis fuga et ipsa repellat cupiditate et.","avatar":{"url":null}},"avatar_url":null,"star_count":0,"forks_count":0,"only_allow_merge_if_build_succeeds":false,"open_issues_count":5,"permissions":{"project_access":null,"group_access":{"access_level":50,"notification_level":3}}}]
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
new file mode 100644
index 00000000000..b529ea6458d
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -0,0 +1,119 @@
+/*= require jquery */
+/*= require gl_dropdown */
+/*= require turbolinks */
+/*= require lib/utils/common_utils */
+/*= require lib/utils/type_utility */
+
+(() => {
+ const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
+
+ const ARROW_KEYS = {
+ DOWN: 40,
+ UP: 38,
+ ENTER: 13,
+ ESC: 27
+ };
+
+ let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ i = i || 0;
+ if (!i) direction = direction.toUpperCase();
+ $('body').trigger({
+ type: 'keydown',
+ which: ARROW_KEYS[direction],
+ keyCode: ARROW_KEYS[direction]
+ });
+ i++;
+ if (i <= steps) {
+ navigateWithKeys(direction, steps, cb, i);
+ } else {
+ cb();
+ }
+ };
+
+ describe('Dropdown', function describeDropdown() {
+ fixture.preload('gl_dropdown.html');
+ fixture.preload('projects.json');
+
+ beforeEach(() => {
+ fixture.load('gl_dropdown.html');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = fixture.load('projects.json')[0];
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ selectable: true,
+ data: this.projectsData,
+ text: (project) => {
+ (project.name_with_namespace || project.name);
+ },
+ id: (project) => {
+ project.id;
+ }
+ });
+ });
+
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
+
+ it('should open on click', () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
+
+ describe('that is open', () => {
+ beforeEach(() => {
+ this.dropdownButtonElement.click();
+ });
+
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+ });
+
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open')
+ let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(Turbolinks, 'visit').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ let linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+ });
+ });
+ });
+
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
new file mode 100644
index 00000000000..840c7b6d015
--- /dev/null
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -0,0 +1,89 @@
+//= require lib/utils/type_utility
+//= require jquery
+//= require bootstrap
+//= require gl_dropdown
+//= require select2
+//= require jquery.nicescroll
+//= require api
+//= require create_label
+//= require issuable_context
+//= require users_select
+//= require labels_select
+
+(() => {
+ let saveLabelCount = 0;
+ describe('Issue dropdown sidebar', () => {
+ fixture.preload('issue_sidebar_label.html');
+
+ beforeEach(() => {
+ fixture.load('issue_sidebar_label.html');
+ new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
+ new LabelsSelect();
+
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const d = $.Deferred();
+ let LABELS_DATA = []
+
+ if (req.url === '/root/test/labels.json') {
+ for (let i = 0; i < 10; i++) {
+ LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ }
+ } else if (req.url === '/root/test/issues/2.json') {
+ let tmp = []
+ for (let i = 0; i < saveLabelCount; i++) {
+ tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ }
+ LABELS_DATA = {labels: tmp};
+ }
+
+ d.resolve(LABELS_DATA);
+ return d.promise();
+ });
+ });
+
+ it('changes collapsed tooltip when changing labels when less than 5', (done) => {
+ saveLabelCount = 5;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdow-content a').each((i, $link) => {
+ if (i < 5) {
+ $link.get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4');
+ done();
+ }, 0);
+ }, 0);
+ });
+
+ it('changes collapsed tooltip when changing labels when more than 5', (done) => {
+ saveLabelCount = 6;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdow-content a').each((i, $link) => {
+ if (i < 5) {
+ $link.get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more');
+ done();
+ }, 0);
+ }, 0);
+ });
+ });
+})();
+
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 68d64483d67..324f5152780 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -105,13 +105,13 @@
a3 = "a[href='" + mrsAssignedToMeLink + "']";
a4 = "a[href='" + mrsIHaveCreatedLink + "']";
expect(list.find(a1).length).toBe(1);
- expect(list.find(a1).text()).toBe(' Issues assigned to me ');
+ expect(list.find(a1).text()).toBe('Issues assigned to me');
expect(list.find(a2).length).toBe(1);
- expect(list.find(a2).text()).toBe(" Issues I've created ");
+ expect(list.find(a2).text()).toBe("Issues I've created");
expect(list.find(a3).length).toBe(1);
- expect(list.find(a3).text()).toBe(' Merge requests assigned to me ');
+ expect(list.find(a3).text()).toBe('Merge requests assigned to me');
expect(list.find(a4).length).toBe(1);
- return expect(list.find(a4).text()).toBe(" Merge requests I've created ");
+ return expect(list.find(a4).text()).toBe("Merge requests I've created");
};
describe('Search autocomplete dropdown', function() {
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 36c77206a3f..e10c1f5c547 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -30,26 +30,35 @@ describe ExtractsPath, lib: true do
expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
end
- context 'escaped slash character in ref' do
- let(:ref) { 'improve%2Fawesome' }
+ context 'ref contains %20' do
+ let(:ref) { 'foo%20bar' }
+
+ it 'is not converted to a space in @id' do
+ @project.repository.add_branch(@project.owner, 'foo%20bar', 'master')
- it 'has no escape sequences in @ref or @logs_path' do
assign_ref_vars
- expect(@ref).to eq('improve/awesome')
- expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
+ expect(@id).to start_with('foo%20bar/')
end
end
- context 'ref contains %20' do
- let(:ref) { 'foo%20bar' }
+ context 'path contains space' do
+ let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
- it 'is not converted to a space in @id' do
- @project.repository.add_branch(@project.owner, 'foo%20bar', 'master')
+ it 'is not converted to %20 in @path' do
+ assign_ref_vars
+
+ expect(@path).to eq(params[:path])
+ end
+ end
+
+ context 'subclass overrides get_id' do
+ it 'uses ref returned by get_id' do
+ allow_any_instance_of(self.class).to receive(:get_id){ '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' }
assign_ref_vars
- expect(@id).to start_with('foo%20bar/')
+ expect(@id).to eq(get_id)
end
end
end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
index 1ff49602486..ab0cce6e091 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -44,45 +44,49 @@ describe Gitlab::Badge::Coverage::Report do
end
end
- context 'pipeline exists' do
- let!(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: 'master')
- end
+ context 'when latest successful pipeline exists' do
+ before do
+ create_pipeline do |pipeline|
+ create(:ci_build, :success, pipeline: pipeline, name: 'first', coverage: 40)
+ create(:ci_build, :success, pipeline: pipeline, coverage: 60)
+ end
- context 'builds exist' do
- before do
- create(:ci_build, name: 'first', pipeline: pipeline, coverage: 40)
- create(:ci_build, pipeline: pipeline, coverage: 60)
+ create_pipeline do |pipeline|
+ create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
end
+ end
- context 'particular job specified' do
- let(:job_name) { 'first' }
+ context 'when particular job specified' do
+ let(:job_name) { 'first' }
- it 'returns coverage for the particular job' do
- expect(badge.status).to eq 40
- end
+ it 'returns coverage for the particular job' do
+ expect(badge.status).to eq 40
end
+ end
- context 'particular job not specified' do
- let(:job_name) { '' }
+ context 'when particular job not specified' do
+ let(:job_name) { '' }
+
+ it 'returns arithemetic mean for the pipeline' do
+ expect(badge.status).to eq 50
+ end
+ end
+ end
- it 'returns arithemetic mean for the pipeline' do
- expect(badge.status).to eq 50
- end
+ context 'when only failed pipeline exists' do
+ before do
+ create_pipeline do |pipeline|
+ create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
end
end
- context 'builds do not exist' do
- it_behaves_like 'unknown coverage report'
+ it_behaves_like 'unknown coverage report'
- context 'particular job specified' do
- let(:job_name) { 'nonexistent' }
+ context 'particular job specified' do
+ let(:job_name) { 'nonexistent' }
- it 'retruns nil' do
- expect(badge.status).to be_nil
- end
+ it 'retruns nil' do
+ expect(badge.status).to be_nil
end
end
end
@@ -90,4 +94,13 @@ describe Gitlab::Badge::Coverage::Report do
context 'pipeline does not exist' do
it_behaves_like 'unknown coverage report'
end
+
+ def create_pipeline
+ opts = { project: project, sha: project.commit.id, ref: 'master' }
+
+ create(:ci_pipeline, opts).tap do |pipeline|
+ yield pipeline
+ pipeline.build_updated
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb b/spec/lib/gitlab/ci/config/node/hidden_spec.rb
index cc44e2cc054..61e2a554419 100644
--- a/spec/lib/gitlab/ci/config/node/hidden_job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/hidden_spec.rb
@@ -1,15 +1,15 @@
require 'spec_helper'
-describe Gitlab::Ci::Config::Node::HiddenJob do
+describe Gitlab::Ci::Config::Node::Hidden do
let(:entry) { described_class.new(config) }
describe 'validations' do
context 'when entry config value is correct' do
- let(:config) { { image: 'ruby:2.2' } }
+ let(:config) { [:some, :array] }
describe '#value' do
it 'returns key value' do
- expect(entry.value).to eq(image: 'ruby:2.2')
+ expect(entry.value).to eq [:some, :array]
end
end
@@ -21,17 +21,6 @@ describe Gitlab::Ci::Config::Node::HiddenJob do
end
context 'when entry value is not correct' do
- context 'incorrect config value type' do
- let(:config) { ['incorrect'] }
-
- describe '#errors' do
- it 'saves errors' do
- expect(entry.errors)
- .to include 'hidden job config should be a hash'
- end
- end
- end
-
context 'when config is empty' do
let(:config) { {} }
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
index b8d9c70479c..ae2c88aac37 100644
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -74,7 +74,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
expect(entry.descendants.first(2))
.to all(be_an_instance_of(Gitlab::Ci::Config::Node::Job))
expect(entry.descendants.last)
- .to be_an_instance_of(Gitlab::Ci::Config::Node::HiddenJob)
+ .to be_an_instance_of(Gitlab::Ci::Config::Node::Hidden)
end
end
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
index 65a828accde..a1d2ca1e272 100644
--- a/spec/lib/gitlab/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -183,6 +183,11 @@ CONFLICT
expect { parse_text('a' * 102401) }.
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
+
+ it 'raises UnsupportedEncoding when the file contains non-UTF-8 characters' do
+ expect { parse_text("a\xC4\xFC".force_encoding(Encoding::ASCII_8BIT)) }.
+ to raise_error(Gitlab::Conflict::Parser::UnsupportedEncoding)
+ end
end
end
end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 10537bea008..6e8fff6f516 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -339,6 +339,48 @@ describe Gitlab::Diff::Position, lib: true do
end
end
+ describe "position for a file in the initial commit" do
+ let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
+
+ subject do
+ described_class.new(
+ old_path: "README.md",
+ new_path: "README.md",
+ old_line: nil,
+ new_line: 1,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.new_file).to be true
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq("+testme")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
describe "#to_json" do
let(:hash) do
{
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index e1153154778..a5cc7b02936 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require_relative '../email_shared_blocks'
-describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
+xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
include_context :email_shared_context
it_behaves_like :email_shared_examples
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
new file mode 100644
index 00000000000..b7c3bc4e1a7
--- /dev/null
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -0,0 +1,132 @@
+require 'spec_helper'
+
+describe Gitlab::GithubImport::Importer, lib: true do
+ describe '#execute' do
+ context 'when an error occurs' do
+ let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_enabled: false) }
+ let(:octocat) { double(id: 123456, login: 'octocat') }
+ let(:created_at) { DateTime.strptime('2011-01-26T19:01:12Z') }
+ let(:updated_at) { DateTime.strptime('2011-01-27T19:01:12Z') }
+ let(:repository) { double(id: 1, fork: false) }
+ let(:source_sha) { create(:commit, project: project).id }
+ let(:source_branch) { double(ref: 'feature', repo: repository, sha: source_sha) }
+ let(:target_sha) { create(:commit, project: project, git_commit: RepoHelpers.another_sample_commit).id }
+ let(:target_branch) { double(ref: 'master', repo: repository, sha: target_sha) }
+
+ let(:label) do
+ double(
+ name: 'Bug',
+ color: 'ff0000',
+ url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
+ )
+ end
+
+ let(:milestone) do
+ double(
+ number: 1347,
+ state: 'open',
+ title: '1.0',
+ description: 'Version 1.0',
+ due_on: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/milestones/1'
+ )
+ end
+
+ let(:issue1) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'Found a bug',
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347'
+ )
+ end
+
+ let(:issue2) do
+ double(
+ number: 1348,
+ milestone: nil,
+ state: 'open',
+ title: nil,
+ body: "I'm having a problem with this.",
+ assignee: nil,
+ user: octocat,
+ comments: 0,
+ pull_request: nil,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348'
+ )
+ end
+
+ let(:pull_request) do
+ double(
+ number: 1347,
+ milestone: nil,
+ state: 'open',
+ title: 'New feature',
+ body: 'Please pull these awesome changes',
+ head: source_branch,
+ base: target_branch,
+ assignee: nil,
+ user: octocat,
+ created_at: created_at,
+ updated_at: updated_at,
+ closed_at: nil,
+ merged_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ )
+ end
+
+ before do
+ allow(project).to receive(:import_data).and_return(double.as_null_object)
+ allow_any_instance_of(Octokit::Client).to receive(:rate_limit!).and_raise(Octokit::NotFound)
+ allow_any_instance_of(Octokit::Client).to receive(:labels).and_return([label, label])
+ allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
+ allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+ allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
+ allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
+ end
+
+ it 'returns true' do
+ expect(described_class.new(project).execute).to eq true
+ end
+
+ it 'does not raise an error' do
+ expect { described_class.new(project).execute }.not_to raise_error
+ end
+
+ it 'stores error messages' do
+ error = {
+ message: 'The remote data could not be fully imported.',
+ errors: [
+ { type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title has already been taken" },
+ { type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" },
+ { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1347", errors: "Invalid Repository. Use user/repo format." },
+ { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
+ { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
+ { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
+ { type: :wiki, errors: "Gitlab::Shell::Error" }
+ ]
+ }
+
+ described_class.new(project).execute
+
+ expect(project.import_error).to eq error.to_json
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
index aa28e360993..b667abf063d 100644
--- a/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/pull_request_formatter_spec.rb
@@ -27,7 +27,8 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
- merged_at: nil
+ merged_at: nil,
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
}
end
@@ -229,4 +230,12 @@ describe Gitlab::GithubImport::PullRequestFormatter, lib: true do
end
end
end
+
+ describe '#url' do
+ let(:raw_data) { double(base_data) }
+
+ it 'return raw url' do
+ expect(pull_request.url).to eq 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb b/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
deleted file mode 100644
index 946712ca38e..00000000000
--- a/spec/lib/gitlab/gitorious_import/project_creator_spec.rb
+++ /dev/null
@@ -1,26 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::GitoriousImport::ProjectCreator, lib: true do
- let(:user) { create(:user) }
- let(:repo) { Gitlab::GitoriousImport::Repository.new('foo/bar-baz-qux') }
- let(:namespace){ create(:group, owner: user) }
-
- before do
- namespace.add_owner(user)
- end
-
- it 'creates project' do
- allow_any_instance_of(Project).to receive(:add_import_job)
-
- project_creator = Gitlab::GitoriousImport::ProjectCreator.new(repo, namespace, user)
- project = project_creator.execute
-
- expect(project.name).to eq("Bar Baz Qux")
- expect(project.path).to eq("bar-baz-qux")
- expect(project.namespace).to eq(namespace)
- expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::PUBLIC)
- expect(project.import_type).to eq("gitorious")
- expect(project.import_source).to eq("foo/bar-baz-qux")
- expect(project.import_url).to eq("https://gitorious.org/foo/bar-baz-qux.git")
- end
-end
diff --git a/spec/lib/gitlab/metrics/rack_middleware_spec.rb b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
index a30cb2a5e38..bcaffd27909 100644
--- a/spec/lib/gitlab/metrics/rack_middleware_spec.rb
+++ b/spec/lib/gitlab/metrics/rack_middleware_spec.rb
@@ -19,7 +19,7 @@ describe Gitlab::Metrics::RackMiddleware do
end
it 'tags a transaction with the name and action of a controller' do
- klass = double(:klass, name: 'TestController')
+ klass = double(:klass, name: 'TestController', content_type: 'text/html')
controller = double(:controller, class: klass, action_name: 'show')
env['action_controller.instance'] = controller
@@ -32,7 +32,7 @@ describe Gitlab::Metrics::RackMiddleware do
middleware.call(env)
end
- it 'tags a transaction with the method andpath of the route in the grape endpoint' do
+ it 'tags a transaction with the method and path of the route in the grape endpoint' do
route = double(:route, route_method: "GET", route_path: "/:version/projects/:id/archive(.:format)")
endpoint = double(:endpoint, route: route)
@@ -87,17 +87,30 @@ describe Gitlab::Metrics::RackMiddleware do
describe '#tag_controller' do
let(:transaction) { middleware.transaction_from_env(env) }
+ let(:content_type) { 'text/html' }
- it 'tags a transaction with the name and action of a controller' do
+ before do
klass = double(:klass, name: 'TestController')
- controller = double(:controller, class: klass, action_name: 'show')
+ controller = double(:controller, class: klass, action_name: 'show', content_type: content_type)
env['action_controller.instance'] = controller
+ end
+ it 'tags a transaction with the name and action of a controller' do
middleware.tag_controller(transaction, env)
expect(transaction.action).to eq('TestController#show')
end
+
+ context 'when the response content type is not :html' do
+ let(:content_type) { 'application/json' }
+
+ it 'appends the mime type to the transaction action' do
+ middleware.tag_controller(transaction, env)
+
+ expect(transaction.action).to eq('TestController#show.json')
+ end
+ end
end
describe '#tag_endpoint' do
diff --git a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
index fd6f684db0c..168090d5b5c 100644
--- a/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
+++ b/spec/lib/gitlab/middleware/rails_queue_duration_spec.rb
@@ -22,7 +22,7 @@ describe Gitlab::Middleware::RailsQueueDuration do
end
it 'sets proxy_flight_time and calls the app when the header is present' do
- env['HTTP_GITLAB_WORHORSE_PROXY_START'] = '123'
+ env['HTTP_GITLAB_WORKHORSE_PROXY_START'] = '123'
expect(transaction).to receive(:set).with(:rails_queue_duration, an_instance_of(Float))
expect(middleware.call(env)).to eq('yay')
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index fa241867858..eae9c060c38 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -493,7 +493,12 @@ describe Notify do
end
def invite_to_project(project:, email:, inviter:)
- ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ project.project_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
project.project_members.invite.last
end
@@ -740,7 +745,12 @@ describe Notify do
end
def invite_to_group(group:, email:, inviter:)
- GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ group.group_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
group.group_members.invite.last
end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 853f6943cef..c50ca38bdd9 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -171,6 +171,70 @@ describe Ability, lib: true do
end
end
+ shared_examples_for ".project_abilities" do |enable_request_store|
+ before do
+ RequestStore.begin! if enable_request_store
+ end
+
+ after do
+ if enable_request_store
+ RequestStore.end!
+ RequestStore.clear!
+ end
+ end
+
+ describe '.project_abilities' do
+ let!(:project) { create(:empty_project, :public) }
+ let!(:user) { create(:user) }
+
+ it 'returns permissions for admin user' do
+ admin = create(:admin)
+
+ results = described_class.project_abilities(admin, project)
+
+ expect(results.count).to eq(68)
+ end
+
+ it 'returns permissions for an owner' do
+ results = described_class.project_abilities(project.owner, project)
+
+ expect(results.count).to eq(68)
+ end
+
+ it 'returns permissions for a master' do
+ project.team << [user, :master]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(60)
+ end
+
+ it 'returns permissions for a developer' do
+ project.team << [user, :developer]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(44)
+ end
+
+ it 'returns permissions for a guest' do
+ project.team << [user, :guest]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(21)
+ end
+ end
+ end
+
+ describe '.project_abilities with RequestStore' do
+ it_behaves_like ".project_abilities", true
+ end
+
+ describe '.project_abilities without RequestStore' do
+ it_behaves_like ".project_abilities", false
+ end
+
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
@@ -218,4 +282,17 @@ describe Ability, lib: true do
end
end
end
+
+ describe '.project_disabled_features_rules' do
+ let(:project) { build(:project) }
+
+ subject { described_class.project_disabled_features_rules(project) }
+
+ context 'wiki named abilities' do
+ it 'disables wiki abilities if the project has no wiki' do
+ expect(project).to receive(:has_wiki?).and_return(false)
+ expect(subject).to include(:read_wiki, :create_wiki, :update_wiki, :admin_wiki)
+ end
+ end
+ end
end
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 72688137f08..02d6263094a 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
- include ActiveSupport::Testing::TimeHelpers
-
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index ee2c3d04984..c45c2635cf4 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -948,15 +948,17 @@ describe Ci::Build, models: true do
before { build.run! }
it 'returns false' do
- expect(build.retryable?).to be false
+ expect(build).not_to be_retryable
end
end
context 'when build is finished' do
- before { build.success! }
+ before do
+ build.success!
+ end
it 'returns true' do
- expect(build.retryable?).to be true
+ expect(build).to be_retryable
end
end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 8137e9f8f71..598df576001 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -124,17 +124,21 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
- let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current }
- let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current }
+ let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
describe '#duration' do
before do
- build.skip
- build2.skip
+ travel_to(current - 120) do
+ pipeline.run
+ end
+
+ travel_to(current) do
+ pipeline.succeed
+ end
end
it 'matches sum of builds duration' do
- expect(pipeline.reload.duration).to eq(build.duration + build2.duration)
+ expect(pipeline.reload.duration).to eq(120)
end
end
@@ -191,6 +195,36 @@ describe Ci::Pipeline, models: true do
end
end
+ context 'with non-empty project' do
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ ref: project.default_branch,
+ sha: project.commit.sha)
+ end
+
+ describe '#latest?' do
+ context 'with latest sha' do
+ it 'returns true' do
+ expect(pipeline).to be_latest
+ end
+ end
+
+ context 'with not latest sha' do
+ before do
+ pipeline.update(
+ sha: project.commit("#{project.default_branch}~1").sha)
+ end
+
+ it 'returns false' do
+ expect(pipeline).not_to be_latest
+ end
+ end
+ end
+ end
+
describe '#manual_actions' do
subject { pipeline.manual_actions }
diff --git a/spec/models/concerns/statuseable_spec.rb b/spec/models/concerns/has_status_spec.rb
index 8e0a2a2cbde..e118432d098 100644
--- a/spec/models/concerns/statuseable_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -1,9 +1,9 @@
require 'spec_helper'
-describe Statuseable do
+describe HasStatus do
before do
@object = Object.new
- @object.extend(Statuseable::ClassMethods)
+ @object.extend(HasStatus::ClassMethods)
end
describe '.status' do
@@ -12,7 +12,7 @@ describe Statuseable do
end
subject { @object.status }
-
+
shared_examples 'build status summary' do
context 'all successful' do
let(:statuses) { Array.new(2) { create(type, status: :success) } }
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2277f4e13bf..fef90d9b5cb 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -65,11 +65,21 @@ describe Member, models: true do
@master_user = create(:user).tap { |u| project.team << [u, :master] }
@master = project.members.find_by(user_id: @master_user.id)
- ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ Member.add_user(
+ project.members,
+ 'toto1@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @master_user
+ )
@invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
accepted_invite_user = build(:user)
- ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ Member.add_user(
+ project.members,
+ 'toto2@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @master_user
+ )
@accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 29f7396f862..e5b185dc3f6 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -1,6 +1,27 @@
require 'spec_helper'
describe MergeRequestDiff, models: true do
+ describe 'create new record' do
+ subject { create(:merge_request).merge_request_diff }
+
+ it { expect(subject).to be_valid }
+ it { expect(subject).to be_persisted }
+ it { expect(subject.commits.count).to eq(5) }
+ it { expect(subject.diffs.count).to eq(8) }
+ it { expect(subject.head_commit_sha).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
+ it { expect(subject.base_commit_sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ it { expect(subject.start_commit_sha).to eq('0b4bc9a49b562e85de7cc9e834518ea6828729b9') }
+ end
+
+ describe '#latest' do
+ let!(:mr) { create(:merge_request, :with_diffs) }
+ let!(:first_diff) { mr.merge_request_diff }
+ let!(:last_diff) { mr.create_merge_request_diff }
+
+ it { expect(last_diff.latest?).to be_truthy }
+ it { expect(first_diff.latest?).to be_falsey }
+ end
+
describe '#diffs' do
let(:mr) { create(:merge_request, :with_diffs) }
let(:mr_diff) { mr.merge_request_diff }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 64c56d922ff..5bf3b8e609e 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -9,7 +9,7 @@ describe MergeRequest, models: true do
it { is_expected.to belong_to(:target_project).with_foreign_key(:target_project_id).class_name('Project') }
it { is_expected.to belong_to(:source_project).with_foreign_key(:source_project_id).class_name('Project') }
it { is_expected.to belong_to(:merge_user).class_name("User") }
- it { is_expected.to have_one(:merge_request_diff).dependent(:destroy) }
+ it { is_expected.to have_many(:merge_request_diffs).dependent(:destroy) }
end
describe 'modules' do
@@ -159,7 +159,7 @@ describe MergeRequest, models: true do
context 'when there are MR diffs' do
it 'delegates to the MR diffs' do
- merge_request.merge_request_diff = MergeRequestDiff.new
+ merge_request.save
expect(merge_request.merge_request_diff).to receive(:raw_diffs).with(hash_including(options))
@@ -316,7 +316,7 @@ describe MergeRequest, models: true do
end
it "can be removed if the last commit is the head of the source branch" do
- allow(subject.source_project).to receive(:commit).and_return(subject.diff_head_commit)
+ allow(subject).to receive(:source_branch_head).and_return(subject.diff_head_commit)
expect(subject.can_remove_source_branch?(user)).to be_truthy
end
@@ -477,8 +477,8 @@ describe MergeRequest, models: true do
allow(subject).to receive(:diff_head_sha).and_return('123abc')
- expect(subject.source_project).to receive(:pipeline).
- with('123abc', 'master').
+ expect(subject.source_project).to receive(:pipeline_for).
+ with('master', '123abc').
and_return(pipeline)
expect(subject.pipeline).to eq(pipeline)
@@ -721,12 +721,15 @@ describe MergeRequest, models: true do
let(:commit) { subject.project.commit(sample_commit.id) }
- it "reloads the diff content" do
- expect(subject.merge_request_diff).to receive(:reload_content)
-
+ it "does not change existing merge request diff" do
+ expect(subject.merge_request_diff).not_to receive(:save_git_content)
subject.reload_diff
end
+ it "creates new merge request diff" do
+ expect { subject.reload_diff }.to change { subject.merge_request_diffs.count }.by(1)
+ end
+
it "executs diff cache service" do
expect_any_instance_of(MergeRequests::MergeRequestDiffCacheService).to receive(:execute).with(subject)
@@ -736,13 +739,15 @@ describe MergeRequest, models: true do
it "updates diff note positions" do
old_diff_refs = subject.diff_refs
- merge_request_diff = subject.merge_request_diff
-
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
- allow(merge_request_diff).to receive(:reload_content) do
- merge_request_diff.base_commit_sha = commit.parent_id
- merge_request_diff.start_commit_sha = commit.parent_id
- merge_request_diff.head_commit_sha = commit.sha
+ allow(subject).to receive(:create_merge_request_diff) do
+ subject.merge_request_diffs.create(
+ base_commit_sha: commit.parent_id,
+ start_commit_sha: commit.parent_id,
+ head_commit_sha: commit.sha
+ )
+
+ subject.merge_request_diff(true)
end
expect(Notes::DiffPositionUpdateService).to receive(:new).with(
@@ -752,14 +757,31 @@ describe MergeRequest, models: true do
new_diff_refs: commit.diff_refs,
paths: note.position.paths
).and_call_original
- expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
+ expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
expect_any_instance_of(DiffNote).to receive(:save).once
subject.reload_diff
end
end
+ describe '#branch_merge_base_commit' do
+ context 'source and target branch exist' do
+ it { expect(subject.branch_merge_base_commit.sha).to eq('ae73cb07c9eeaf35924a10f713b364d32b2dd34f') }
+ it { expect(subject.branch_merge_base_commit).to be_a(Commit) }
+ end
+
+ context 'when the target branch does not exist' do
+ before do
+ subject.project.repository.raw_repository.delete_branch(subject.target_branch)
+ end
+
+ it 'returns nil' do
+ expect(subject.branch_merge_base_commit).to be_nil
+ end
+ end
+ end
+
describe "#diff_sha_refs" do
context "with diffs" do
subject { create(:merge_request, :with_diffs) }
@@ -890,6 +912,19 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
end
+ it 'returns a falsey value when the MR is marked as having conflicts, but has none' do
+ merge_request = create_merge_request('master')
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
+ it 'returns a falsey value when the MR has a missing ref after a force push' do
+ merge_request = create_merge_request('conflict-resolvable')
+ allow(merge_request.conflicts).to receive(:merge_index).and_raise(Rugged::OdbError)
+
+ expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_falsey
+ end
+
it 'returns a falsey value when the MR does not support new diff notes' do
merge_request = create_merge_request('conflict-resolvable')
merge_request.merge_request_diff.update_attributes(start_commit_sha: nil)
@@ -927,4 +962,80 @@ describe MergeRequest, models: true do
expect(merge_request.conflicts_can_be_resolved_in_ui?).to be_truthy
end
end
+
+ describe "#forked_source_project_missing?" do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:user) { create(:user) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+ context "when the fork exists" do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it { expect(merge_request.forked_source_project_missing?).to be_falsey }
+ end
+
+ context "when the source project is the same as the target project" do
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ it { expect(merge_request.forked_source_project_missing?).to be_falsey }
+ end
+
+ context "when the fork does not exist" do
+ let(:merge_request) do
+ create(:merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it "returns true" do
+ unlink_project.execute
+ merge_request.reload
+
+ expect(merge_request.forked_source_project_missing?).to be_truthy
+ end
+ end
+ end
+
+ describe "#closed_without_fork?" do
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:user) { create(:user) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+ context "when the merge request is closed" do
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it "returns false if the fork exist" do
+ expect(closed_merge_request.closed_without_fork?).to be_falsey
+ end
+
+ it "returns true if the fork does not exist" do
+ unlink_project.execute
+ closed_merge_request.reload
+
+ expect(closed_merge_request.closed_without_fork?).to be_truthy
+ end
+ end
+
+ context "when the merge request is open" do
+ let(:open_merge_request) do
+ create(:merge_request,
+ source_project: fork_project,
+ target_project: project)
+ end
+
+ it "returns false" do
+ expect(open_merge_request.closed_without_fork?).to be_falsey
+ end
+ end
+ end
end
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
new file mode 100644
index 00000000000..b76513d2a3c
--- /dev/null
+++ b/spec/models/network/graph_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Network::Graph, models: true do
+ let(:project) { create(:project) }
+ let!(:note_on_commit) { create(:note_on_commit, project: project) }
+
+ it '#initialize' do
+ graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
+
+ expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
+ end
+end
diff --git a/spec/models/note_spec.rb b/spec/models/note_spec.rb
index ef2747046b9..9e8ae07e0b2 100644
--- a/spec/models/note_spec.rb
+++ b/spec/models/note_spec.rb
@@ -225,7 +225,7 @@ describe Note, models: true do
let(:note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
system: true
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index d1f3a815290..dd309ea1b68 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -247,7 +247,7 @@ describe Project, models: true do
end
end
- describe "#new_issue_address" do
+ xdescribe "#new_issue_address" do
let(:project) { create(:empty_project, path: "somewhere") }
let(:user) { create(:user) }
@@ -506,6 +506,18 @@ describe Project, models: true do
end
end
+ describe '#has_wiki?' do
+ let(:no_wiki_project) { build(:project, wiki_enabled: false, has_external_wiki: false) }
+ let(:wiki_enabled_project) { build(:project, wiki_enabled: true) }
+ let(:external_wiki_project) { build(:project, has_external_wiki: true) }
+
+ it 'returns true if project is wiki enabled or has external wiki' do
+ expect(wiki_enabled_project).to have_wiki
+ expect(external_wiki_project).to have_wiki
+ expect(no_wiki_project).not_to have_wiki
+ end
+ end
+
describe '#external_wiki' do
let(:project) { create(:project) }
@@ -685,23 +697,37 @@ describe Project, models: true do
end
end
- describe '#pipeline' do
- let(:project) { create :project }
- let(:pipeline) { create :ci_pipeline, project: project, ref: 'master' }
-
- subject { project.pipeline(pipeline.sha, 'master') }
+ describe '#pipeline_for' do
+ let(:project) { create(:project) }
+ let!(:pipeline) { create_pipeline }
- it { is_expected.to eq(pipeline) }
+ shared_examples 'giving the correct pipeline' do
+ it { is_expected.to eq(pipeline) }
- context 'return latest' do
- let(:pipeline2) { create :ci_pipeline, project: project, ref: 'master' }
+ context 'return latest' do
+ let!(:pipeline2) { create_pipeline }
- before do
- pipeline
- pipeline2
+ it { is_expected.to eq(pipeline2) }
end
+ end
+
+ context 'with explicit sha' do
+ subject { project.pipeline_for('master', pipeline.sha) }
+
+ it_behaves_like 'giving the correct pipeline'
+ end
+
+ context 'with implicit sha' do
+ subject { project.pipeline_for('master') }
+
+ it_behaves_like 'giving the correct pipeline'
+ end
- it { is_expected.to eq(pipeline2) }
+ def create_pipeline
+ create(:ci_pipeline,
+ project: project,
+ ref: 'master',
+ sha: project.commit('master').sha)
end
end
@@ -1442,4 +1468,35 @@ describe Project, models: true do
expect(shared_project.authorized_for_user?(master, Gitlab::Access::MASTER)).to be(true)
end
end
+
+ describe 'change_head' do
+ let(:project) { create(:project) }
+
+ it 'calls the before_change_head method' do
+ expect(project.repository).to receive(:before_change_head)
+ project.change_head(project.default_branch)
+ end
+
+ it 'creates the new reference with rugged' do
+ expect(project.repository.rugged.references).to receive(:create).with('HEAD',
+ "refs/heads/#{project.default_branch}",
+ force: true)
+ project.change_head(project.default_branch)
+ end
+
+ it 'copies the gitattributes' do
+ expect(project.repository).to receive(:copy_gitattributes).with(project.default_branch)
+ project.change_head(project.default_branch)
+ end
+
+ it 'expires the avatar cache' do
+ expect(project.repository).to receive(:expire_avatar_cache).with(project.default_branch)
+ project.change_head(project.default_branch)
+ end
+
+ it 'reloads the default branch' do
+ expect(project).to receive(:reload_default_branch)
+ project.change_head(project.default_branch)
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f7dbfd712cc..1fea50ad42c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -719,6 +719,14 @@ describe Repository, models: true do
expect(merge_commit).to be_present
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
end
+
+ it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
+ merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
+ merge_commit_id = repository.merge(user, merge_request, commit_options)
+ repository.commit(merge_commit_id)
+
+ expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
+ end
end
describe '#revert' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 51e4780e2b1..8eb0c5033c9 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -920,6 +920,16 @@ describe User, models: true do
expect(subject.recent_push).to eq(nil)
end
+
+ it "includes push events on any of the provided projects" do
+ expect(subject.recent_push(project1)).to eq(nil)
+ expect(subject.recent_push(project2)).to eq(push_event)
+
+ push_data1 = Gitlab::DataBuilder::Push.build_sample(project1, subject)
+ push_event1 = create(:event, action: Event::PUSHED, project: project1, target: project1, author: subject, data: push_data1)
+
+ expect(subject.recent_push([project1, project2])).to eq(push_event1) # Newest
+ end
end
describe '#authorized_groups' do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index c65510fadec..bbdf8f03c2b 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe API::Helpers, api: true do
include API::Helpers
include ApiHelpers
+ include SentryHelper
let(:user) { create(:user) }
let(:admin) { create(:admin) }
@@ -234,4 +235,30 @@ describe API::Helpers, api: true do
expect(to_boolean(nil)).to be_nil
end
end
+
+ describe '.handle_api_exception' do
+ before do
+ allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
+ allow_any_instance_of(self.class).to receive(:rack_response)
+ end
+
+ it 'does not report a MethodNotAllowed exception to Sentry' do
+ exception = Grape::Exceptions::MethodNotAllowed.new({ 'X-GitLab-Test' => '1' })
+ allow(exception).to receive(:backtrace).and_return(caller)
+
+ expect(Raven).not_to receive(:capture_exception).with(exception)
+
+ handle_api_exception(exception)
+ end
+
+ it 'does report RuntimeError to Sentry' do
+ exception = RuntimeError.new('test error')
+ allow(exception).to receive(:backtrace).and_return(caller)
+
+ expect_any_instance_of(self.class).to receive(:sentry_context)
+ expect(Raven).to receive(:capture_exception).with(exception)
+
+ handle_api_exception(exception)
+ end
+ end
end
diff --git a/spec/requests/api/award_emoji_spec.rb b/spec/requests/api/award_emoji_spec.rb
index 73c268c0d1e..981a6791881 100644
--- a/spec/requests/api/award_emoji_spec.rb
+++ b/spec/requests/api/award_emoji_spec.rb
@@ -4,7 +4,7 @@ describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let!(:project) { create(:project) }
- let(:issue) { create(:issue, project: project, author: user) }
+ let(:issue) { create(:issue, project: project) }
let!(:award_emoji) { create(:award_emoji, awardable: issue, user: user) }
let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
let!(:downvote) { create(:award_emoji, :downvote, awardable: merge_request, user: user) }
@@ -115,6 +115,8 @@ describe API::API, api: true do
end
describe "POST /projects/:id/awardable/:awardable_id/award_emoji" do
+ let(:issue2) { create(:issue, project: project, author: user) }
+
context "on an issue" do
it "creates a new award emoji" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: 'blowfish'
@@ -136,6 +138,12 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
+ it "returns a 404 error if the user authored issue" do
+ post api("/projects/#{project.id}/issues/#{issue2.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ end
+
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.id}/award_emoji", user), name: '+1'
@@ -155,6 +163,8 @@ describe API::API, api: true do
end
describe "POST /projects/:id/awardable/:awardable_id/notes/:note_id/award_emoji" do
+ let(:note2) { create(:note, project: project, noteable: issue, author: user) }
+
it 'creates a new award emoji' do
expect do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: 'rocket'
@@ -164,6 +174,12 @@ describe API::API, api: true do
expect(json_response['user']['username']).to eq(user.username)
end
+ it "it returns 404 error when user authored note" do
+ post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note2.id}/award_emoji", user), name: 'thumbsup'
+
+ expect(response).to have_http_status(404)
+ end
+
it "normalizes +1 as thumbsup award" do
post api("/projects/#{project.id}/issues/#{issue.id}/notes/#{note.id}/award_emoji", user), name: '+1'
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 9a17a705b1e..ee0b61e2ca4 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -15,7 +15,9 @@ describe API::API, api: true do
describe 'GET /projects/:id/builds ' do
let(:query) { '' }
- before { get api("/projects/#{project.id}/builds?#{query}", api_user) }
+ before do
+ get api("/projects/#{project.id}/builds?#{query}", api_user)
+ end
context 'authorized user' do
it 'returns project builds' do
@@ -122,7 +124,9 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/builds/:build_id' do
- before { get api("/projects/#{project.id}/builds/#{build.id}", api_user) }
+ before do
+ get api("/projects/#{project.id}/builds/#{build.id}", api_user)
+ end
context 'authorized user' do
it 'returns specific build data' do
@@ -141,7 +145,9 @@ describe API::API, api: true do
end
describe 'GET /projects/:id/builds/:build_id/artifacts' do
- before { get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user) }
+ before do
+ get api("/projects/#{project.id}/builds/#{build.id}/artifacts", api_user)
+ end
context 'build with artifacts' do
let(:build) { create(:ci_build, :artifacts, pipeline: pipeline) }
@@ -292,7 +298,9 @@ describe API::API, api: true do
end
describe 'POST /projects/:id/builds/:build_id/cancel' do
- before { post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user) }
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/cancel", api_user)
+ end
context 'authorized user' do
context 'user with :update_build persmission' do
@@ -323,7 +331,9 @@ describe API::API, api: true do
describe 'POST /projects/:id/builds/:build_id/retry' do
let(:build) { create(:ci_build, :canceled, pipeline: pipeline) }
- before { post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user) }
+ before do
+ post api("/projects/#{project.id}/builds/#{build.id}/retry", api_user)
+ end
context 'authorized user' do
context 'user with :update_build permission' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 7ca75d77673..5b3dc60aba2 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -95,7 +95,7 @@ describe API::API, api: true do
end
it "returns status for CI" do
- pipeline = project.ensure_pipeline(project.repository.commit.sha, 'master')
+ pipeline = project.ensure_pipeline('master', project.repository.commit.sha)
pipeline.update(status: 'success')
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -105,7 +105,7 @@ describe API::API, api: true do
end
it "returns status for CI when pipeline is created" do
- project.ensure_pipeline(project.repository.commit.sha, 'master')
+ project.ensure_pipeline('master', project.repository.commit.sha)
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index be52f88831f..5d06abcfeb3 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -38,6 +38,68 @@ describe API::API, api: true do
end
end
+ describe 'GET /internal/two_factor_recovery_codes' do
+ it 'returns an error message when the key does not exist' do
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: 12345
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Not found')
+ end
+
+ it 'returns an error message when the key is a deploy key' do
+ deploy_key = create(:deploy_key)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: deploy_key.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Deploy keys cannot be used to retrieve recovery codes')
+ end
+
+ it 'returns an error message when the user does not exist' do
+ key_without_user = create(:key, user: nil)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key_without_user.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find a user for the given key')
+ expect(json_response['recovery_codes']).to be_nil
+ end
+
+ context 'when two-factor is enabled' do
+ it 'returns new recovery codes when the user exists' do
+ allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(true)
+ allow_any_instance_of(User)
+ .to receive(:generate_otp_backup_codes!).and_return(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key.id
+
+ expect(json_response['success']).to be_truthy
+ expect(json_response['recovery_codes']).to match_array(%w(119135e5a3ebce8e 34bd7b74adbc8861))
+ end
+ end
+
+ context 'when two-factor is not enabled' do
+ it 'returns an error message' do
+ allow_any_instance_of(User).to receive(:two_factor_enabled?).and_return(false)
+
+ post api('/internal/two_factor_recovery_codes'),
+ secret_token: secret_token,
+ key_id: key.id
+
+ expect(json_response['success']).to be_falsey
+ expect(json_response['recovery_codes']).to be_nil
+ end
+ end
+ end
+
describe "GET /internal/discover" do
it do
get(api("/internal/discover"), key_id: key.id, secret_token: secret_token)
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index a40e1a93b71..3362a88d798 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -2,6 +2,7 @@ require 'spec_helper'
describe API::API, api: true do
include ApiHelpers
+
let(:user) { create(:user) }
let(:user2) { create(:user) }
let(:non_member) { create(:user) }
@@ -61,6 +62,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first['title']).to eq(issue.title)
+ expect(json_response.last).to have_key('web_url')
end
it "adds pagination headers and keep query params" do
@@ -477,6 +479,18 @@ describe API::API, api: true do
expect(json_response['labels']).to eq(['label', 'label2'])
end
+ it "sends notifications for subscribers of newly added labels" do
+ label = project.labels.first
+ label.toggle_subscription(user2)
+
+ perform_enqueued_jobs do
+ post api("/projects/#{project.id}/issues", user),
+ title: 'new issue', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
it "returns a 400 bad request if title not given" do
post api("/projects/#{project.id}/issues", user), labels: 'label, label2'
expect(response).to have_http_status(400)
@@ -632,6 +646,18 @@ describe API::API, api: true do
expect(json_response['labels']).to eq([label.title])
end
+ it "sends notifications for subscribers of newly added labels when issue is updated" do
+ label = create(:label, title: 'foo', color: '#FFAABB', project: project)
+ label.toggle_subscription(user2)
+
+ perform_enqueued_jobs do
+ put api("/projects/#{project.id}/issues/#{issue.id}", user),
+ title: 'updated title', labels: label.title
+ end
+
+ should_email(user2)
+ end
+
it 'removes all labels' do
put api("/projects/#{project.id}/issues/#{issue.id}", user),
labels: ''
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index a56ee30f7b1..1e365bf353a 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -122,12 +122,13 @@ describe API::Members, api: true do
it 'creates a new member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
- user_id: stranger.id, access_level: Member::DEVELOPER
+ user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
expect(response).to have_http_status(201)
end.to change { source.members.count }.by(1)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
end
end
@@ -183,11 +184,12 @@ describe API::Members, api: true do
context 'when authenticated as a master/owner' do
it 'updates the member' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
- access_level: Member::MASTER
+ access_level: Member::MASTER, expires_at: '2016-08-05'
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(developer.id)
expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
end
end
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
new file mode 100644
index 00000000000..8f1e5ac9891
--- /dev/null
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -0,0 +1,49 @@
+require "spec_helper"
+
+describe API::API, 'MergeRequestDiffs', api: true do
+ include ApiHelpers
+
+ let!(:user) { create(:user) }
+ let!(:merge_request) { create(:merge_request, importing: true) }
+ let!(:project) { merge_request.target_project }
+
+ before do
+ merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ project.team << [user, :master]
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user) }
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response.size).to eq(merge_request.merge_request_diffs.size) }
+ it { expect(json_response.first['id']).to eq(merge_request_diff.id) }
+ it { expect(json_response.first['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_requests/999/versions", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe 'GET /projects/:id/merge_requests/:merge_request_id/versions/:version_id' do
+ context 'valid merge request' do
+ before { get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/#{merge_request_diff.id}", user) }
+ let(:merge_request_diff) { merge_request.merge_request_diffs.first }
+
+ it { expect(response.status).to eq 200 }
+ it { expect(json_response['id']).to eq(merge_request_diff.id) }
+ it { expect(json_response['head_commit_sha']).to eq(merge_request_diff.head_commit_sha) }
+ it { expect(json_response['diffs'].size).to eq(merge_request_diff.diffs.size) }
+ end
+
+ it 'returns a 404 when merge_request_id not found' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions/999", user)
+ expect(response).to have_http_status(404)
+ end
+ end
+end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 617600d6173..baff872e28e 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -33,6 +33,7 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.length).to eq(3)
expect(json_response.last['title']).to eq(merge_request.title)
+ expect(json_response.last).to have_key('web_url')
end
it "returns an array of all merge_requests" do
diff --git a/spec/requests/api/notes_spec.rb b/spec/requests/api/notes_spec.rb
index 737fa14cbb0..223444ea39f 100644
--- a/spec/requests/api/notes_spec.rb
+++ b/spec/requests/api/notes_spec.rb
@@ -25,7 +25,7 @@ describe API::API, api: true do
let!(:cross_reference_note) do
create :note,
noteable: ext_issue, project: ext_proj,
- note: "mentioned in issue #{private_issue.to_reference(ext_proj)}",
+ note: "Mentioned in issue #{private_issue.to_reference(ext_proj)}",
system: true
end
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 914e88c9487..765dc8a8f66 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -34,6 +34,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response.first['note_events']).to eq(true)
expect(json_response.first['build_events']).to eq(true)
expect(json_response.first['pipeline_events']).to eq(true)
+ expect(json_response.first['wiki_page_events']).to eq(true)
expect(json_response.first['enable_ssl_verification']).to eq(true)
end
end
@@ -57,6 +58,9 @@ describe API::API, 'ProjectHooks', api: true do
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['build_events']).to eq(hook.build_events)
+ expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+ expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
@@ -93,6 +97,7 @@ describe API::API, 'ProjectHooks', api: true do
expect(json_response['note_events']).to eq(false)
expect(json_response['build_events']).to eq(false)
expect(json_response['pipeline_events']).to eq(false)
+ expect(json_response['wiki_page_events']).to eq(false)
expect(json_response['enable_ssl_verification']).to eq(true)
end
@@ -118,6 +123,9 @@ describe API::API, 'ProjectHooks', api: true do
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['build_events']).to eq(hook.build_events)
+ expect(json_response['pipeline_events']).to eq(hook.pipeline_events)
+ expect(json_response['wiki_page_events']).to eq(hook.wiki_page_events)
expect(json_response['enable_ssl_verification']).to eq(hook.enable_ssl_verification)
end
diff --git a/spec/requests/api/project_snippets_spec.rb b/spec/requests/api/project_snippets_spec.rb
index 42757ff21b0..01148f0a05e 100644
--- a/spec/requests/api/project_snippets_spec.rb
+++ b/spec/requests/api/project_snippets_spec.rb
@@ -30,6 +30,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response.size).to eq(3)
expect(json_response.map{ |snippet| snippet['id']} ).to include(public_snippet.id, internal_snippet.id, private_snippet.id)
+ expect(json_response.last).to have_key('web_url')
end
it 'hides private snippets from regular user' do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 4742b3d0e37..63f2467be63 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -224,7 +224,8 @@ describe API::API, api: true do
description: FFaker::Lorem.sentence,
issues_enabled: false,
merge_requests_enabled: false,
- wiki_enabled: false
+ wiki_enabled: false,
+ only_allow_merge_if_build_succeeds: false
})
post api('/projects', user), project
@@ -276,6 +277,18 @@ describe API::API, api: true do
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post api('/projects', user), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
+
context 'when a visibility level is restricted' do
before do
@project = attributes_for(:project, { public: true })
@@ -384,6 +397,18 @@ describe API::API, api: true do
expect(json_response['public']).to be_falsey
expect(json_response['visibility_level']).to eq(Gitlab::VisibilityLevel::PRIVATE)
end
+
+ it 'sets a project as allowing merge even if build fails' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: false })
+ post api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_falsey
+ end
+
+ it 'sets a project as allowing merge only if build succeeds' do
+ project = attributes_for(:project, { only_allow_merge_if_build_succeeds: true })
+ post api("/projects/user/#{user.id}", admin), project
+ expect(json_response['only_allow_merge_if_build_succeeds']).to be_truthy
+ end
end
describe "POST /projects/:id/uploads" do
@@ -444,6 +469,7 @@ describe API::API, api: true do
expect(json_response['shared_with_groups'][0]['group_id']).to eq(group.id)
expect(json_response['shared_with_groups'][0]['group_name']).to eq(group.name)
expect(json_response['shared_with_groups'][0]['group_access_level']).to eq(link.group_access)
+ expect(json_response['only_allow_merge_if_build_succeeds']).to eq(project.only_allow_merge_if_build_succeeds)
end
it 'returns a project by path name' do
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 4c9b4a8ba42..fcd6521317a 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -44,6 +44,113 @@ describe 'Git LFS API and storage' do
end
end
+ context 'project specific LFS settings' do
+ let(:project) { create(:empty_project) }
+ let(:body) do
+ {
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ],
+ 'operation' => 'upload'
+ }
+ end
+ let(:authorization) { authorize_user }
+
+ context 'with LFS disabled globally' do
+ before do
+ project.team << [user, :master]
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ end
+
+ describe 'LFS disabled in project' do
+ before do
+ project.update_attribute(:lfs_enabled, false)
+ end
+
+ it 'responds with a 501 message on upload' do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+ expect(response).to have_http_status(501)
+ end
+
+ it 'responds with a 501 message on download' do
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+ expect(response).to have_http_status(501)
+ end
+ end
+
+ describe 'LFS enabled in project' do
+ before do
+ project.update_attribute(:lfs_enabled, true)
+ end
+
+ it 'responds with a 501 message on upload' do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+ expect(response).to have_http_status(501)
+ end
+
+ it 'responds with a 501 message on download' do
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+ expect(response).to have_http_status(501)
+ end
+ end
+ end
+
+ context 'with LFS enabled globally' do
+ before do
+ project.team << [user, :master]
+ enable_lfs
+ end
+
+ describe 'LFS disabled in project' do
+ before do
+ project.update_attribute(:lfs_enabled, false)
+ end
+
+ it 'responds with a 403 message on upload' do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+ expect(response).to have_http_status(403)
+ expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
+ end
+
+ it 'responds with a 403 message on download' do
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+ expect(response).to have_http_status(403)
+ expect(json_response).to include('message' => 'Access forbidden. Check your access level.')
+ end
+ end
+
+ describe 'LFS enabled in project' do
+ before do
+ project.update_attribute(:lfs_enabled, true)
+ end
+
+ it 'responds with a 200 message on upload' do
+ post_lfs_json "#{project.http_url_to_repo}/info/lfs/objects/batch", body, headers
+
+ expect(response).to have_http_status(200)
+ expect(json_response['objects'].first['size']).to eq(1575078)
+ end
+
+ it 'responds with a 200 message on download' do
+ get "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", nil, headers
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+ end
+
describe 'deprecated API' do
let(:project) { create(:empty_project) }
diff --git a/spec/requests/projects/artifacts_controller_spec.rb b/spec/requests/projects/artifacts_controller_spec.rb
new file mode 100644
index 00000000000..e02f0eacc93
--- /dev/null
+++ b/spec/requests/projects/artifacts_controller_spec.rb
@@ -0,0 +1,117 @@
+require 'spec_helper'
+
+describe Projects::ArtifactsController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project,
+ sha: project.commit.sha,
+ ref: project.default_branch,
+ status: 'success')
+ end
+
+ let(:build) { create(:ci_build, :success, :artifacts, pipeline: pipeline) }
+
+ describe 'GET /:project/builds/artifacts/:ref_name/browse?job=name' do
+ before do
+ project.team << [user, :developer]
+
+ login_as(user)
+ end
+
+ def path_from_ref(
+ ref = pipeline.ref, job = build.name, path = 'browse')
+ latest_succeeded_namespace_project_artifacts_path(
+ project.namespace,
+ project,
+ [ref, path].join('/'),
+ job: job)
+ end
+
+ context 'cannot find the build' do
+ shared_examples 'not found' do
+ it { expect(response).to have_http_status(:not_found) }
+ end
+
+ context 'has no such ref' do
+ before do
+ get path_from_ref('TAIL', build.name)
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no such build' do
+ before do
+ get path_from_ref(pipeline.ref, 'NOBUILD')
+ end
+
+ it_behaves_like 'not found'
+ end
+
+ context 'has no path' do
+ before do
+ get path_from_ref(pipeline.sha, build.name, '')
+ end
+
+ it_behaves_like 'not found'
+ end
+ end
+
+ context 'found the build and redirect' do
+ shared_examples 'redirect to the build' do
+ it 'redirects' do
+ path = browse_namespace_project_build_artifacts_path(
+ project.namespace,
+ project,
+ build)
+
+ expect(response).to redirect_to(path)
+ end
+ end
+
+ context 'with regular branch' do
+ before do
+ pipeline.update(ref: 'master',
+ sha: project.commit('master').sha)
+
+ get path_from_ref('master')
+ end
+
+ it_behaves_like 'redirect to the build'
+ end
+
+ context 'with branch name containing slash' do
+ before do
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+
+ get path_from_ref('improve/awesome')
+ end
+
+ it_behaves_like 'redirect to the build'
+ end
+
+ context 'with branch name and path containing slashes' do
+ before do
+ pipeline.update(ref: 'improve/awesome',
+ sha: project.commit('improve/awesome').sha)
+
+ get path_from_ref('improve/awesome', build.name, 'file/README.md')
+ end
+
+ it 'redirects' do
+ path = file_namespace_project_build_artifacts_path(
+ project.namespace,
+ project,
+ build,
+ 'README.md')
+
+ expect(response).to redirect_to(path)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 1d4df9197f6..4bc3cddd9c2 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -107,21 +107,28 @@ describe HelpController, "routing" do
end
it 'to #show' do
- path = '/help/markdown/markdown.md'
+ path = '/help/user/markdown.md'
expect(get(path)).to route_to('help#show',
- path: 'markdown/markdown',
+ path: 'user/markdown',
format: 'md')
path = '/help/workflow/protected_branches/protected_branches1.png'
expect(get(path)).to route_to('help#show',
path: 'workflow/protected_branches/protected_branches1',
format: 'png')
-
+
path = '/help/ui'
expect(get(path)).to route_to('help#ui')
end
end
+# koding GET /koding(.:format) koding#index
+describe KodingController, "routing" do
+ it "to #index" do
+ expect(get("/koding")).to route_to('koding#index')
+ end
+end
+
# profile_account GET /profile/account(.:format) profile#account
# profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index f7f45983d26..cf4c5f13635 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -30,7 +30,7 @@ describe Boards::Issues::ListService, services: true do
let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) }
- let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) }
before do
project.team << [user, :developer]
@@ -58,15 +58,15 @@ describe Boards::Issues::ListService, services: true do
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
+ expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1]
end
- it 'returns opened issues that have label list applied when listing issues from a label list' do
+ it 'returns opened/closed issues that have label list applied when listing issues from a label list' do
params = { id: list1.id }
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
+ expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2]
end
end
end
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index 5e7e145065e..90764b86b16 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -5,7 +5,7 @@ describe Boards::Lists::CreateService, services: true do
let(:project) { create(:project_with_board) }
let(:board) { project.board }
let(:user) { create(:user) }
- let(:label) { create(:label, name: 'in-progress') }
+ let(:label) { create(:label, project: project, name: 'in-progress') }
subject(:service) { described_class.new(project, user, label_id: label.id) }
@@ -50,5 +50,14 @@ describe Boards::Lists::CreateService, services: true do
expect(list2.reload.position).to eq 1
end
end
+
+ context 'when provided label does not belongs to the project' do
+ it 'raises an error' do
+ label = create(:label, name: 'in-development')
+ service = described_class.new(project, user, label_id: label.id)
+
+ expect { service.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ 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
index c931c3e4829..b3e0a7b9b58 100644
--- a/spec/services/ci/image_for_build_service_spec.rb
+++ b/spec/services/ci/image_for_build_service_spec.rb
@@ -5,7 +5,7 @@ module Ci
let(:service) { ImageForBuildService.new }
let(:project) { FactoryGirl.create(:empty_project) }
let(:commit_sha) { '01234567890123456789' }
- let(:pipeline) { project.ensure_pipeline(commit_sha, 'master') }
+ let(:pipeline) { project.ensure_pipeline('master', commit_sha) }
let(:build) { FactoryGirl.create(:ci_build, pipeline: pipeline) }
describe '#execute' do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index ad8c2485888..8326e5cd313 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -3,8 +3,6 @@ require 'spec_helper'
describe Ci::ProcessPipelineService, services: true do
let(:pipeline) { create(:ci_pipeline, ref: 'master') }
let(:user) { create(:user) }
- let(:all_builds) { pipeline.builds }
- let(:builds) { all_builds.where.not(status: [:created, :skipped]) }
let(:config) { nil }
before do
@@ -12,6 +10,14 @@ describe Ci::ProcessPipelineService, services: true do
end
describe '#execute' do
+ def all_builds
+ pipeline.builds
+ end
+
+ def builds
+ all_builds.where.not(status: [:created, :skipped])
+ end
+
def create_builds
described_class.new(pipeline.project, user).execute(pipeline)
end
@@ -48,7 +54,7 @@ describe Ci::ProcessPipelineService, services: true do
it 'does not process pipeline if existing stage is running' do
expect(create_builds).to be_truthy
expect(builds.pending.count).to eq(2)
-
+
expect(create_builds).to be_falsey
expect(builds.pending.count).to eq(2)
end
@@ -224,6 +230,40 @@ describe Ci::ProcessPipelineService, services: true do
end
end
+ context 'when failed build in the middle stage is retried' do
+ context 'when failed build is the only unsuccessful build in the stage' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
+ end
+
+ it 'does trigger builds in the next stage' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
+
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ pipeline.builds.find_by(name: 'test:1').success
+ pipeline.builds.find_by(name: 'test:2').drop
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
+
+ expect(builds.pluck(:name)).to contain_exactly(
+ 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
+ end
+ end
+ end
+
context 'creates a builds from .gitlab-ci.yml' do
let(:config) do
YAML.dump({
diff --git a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
index c4b87468275..807f89e80b7 100644
--- a/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
+++ b/spec/services/merge_requests/merge_request_diff_cache_service_spec.rb
@@ -6,7 +6,7 @@ describe MergeRequests::MergeRequestDiffCacheService do
describe '#execute' do
it 'retrieves the diff files to cache the highlighted result' do
merge_request = create(:merge_request)
- cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequest.default_options]
+ cache_key = [merge_request.merge_request_diff, 'highlighted-diff-files', Gitlab::Diff::FileCollection::MergeRequestDiff.default_options]
expect(Rails.cache).to receive(:read).with(cache_key).and_return({})
expect(Rails.cache).to receive(:write).with(cache_key, anything)
diff --git a/spec/services/merge_requests/resolve_service_spec.rb b/spec/services/merge_requests/resolve_service_spec.rb
new file mode 100644
index 00000000000..d71932458fa
--- /dev/null
+++ b/spec/services/merge_requests/resolve_service_spec.rb
@@ -0,0 +1,87 @@
+require 'spec_helper'
+
+describe MergeRequests::ResolveService do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:fork_project) do
+ create(:forked_project_with_submodules) do |fork_project|
+ fork_project.build_forked_project_link(forked_to_project_id: fork_project.id, forked_from_project_id: project.id)
+ fork_project.save
+ end
+ end
+
+ let(:merge_request) do
+ create(:merge_request,
+ source_branch: 'conflict-resolvable', source_project: project,
+ target_branch: 'conflict-start')
+ end
+
+ let(:merge_request_from_fork) do
+ create(:merge_request,
+ source_branch: 'conflict-resolvable-fork', source_project: fork_project,
+ target_branch: 'conflict-start', target_project: project)
+ end
+
+ describe '#execute' do
+ context 'with valid params' do
+ let(:params) do
+ {
+ sections: {
+ '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_9_9' => 'head',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_21_21' => 'origin',
+ '6eb14e00385d2fb284765eb1cd8d420d33d63fc9_49_49' => 'origin'
+ },
+ commit_message: 'This is a commit message!'
+ }
+ end
+
+ context 'when the source and target project are the same' do
+ before do
+ MergeRequests::ResolveService.new(project, user, params).execute(merge_request)
+ end
+
+ it 'creates a commit with the message' do
+ expect(merge_request.source_branch_head.message).to eq(params[:commit_message])
+ end
+
+ it 'creates a commit with the correct parents' do
+ expect(merge_request.source_branch_head.parents.map(&:id)).
+ to eq(['1450cd639e0bc6721eb02800169e464f212cde06',
+ '75284c70dd26c87f2a3fb65fd5a1f0b0138d3a6b'])
+ end
+ end
+
+ context 'when the source project is a fork and does not contain the HEAD of the target branch' do
+ let!(:target_head) do
+ project.repository.commit_file(user, 'new-file-in-target', '', 'Add new file in target', 'conflict-start', false)
+ end
+
+ before do
+ MergeRequests::ResolveService.new(fork_project, user, params).execute(merge_request_from_fork)
+ end
+
+ it 'creates a commit with the message' do
+ expect(merge_request_from_fork.source_branch_head.message).to eq(params[:commit_message])
+ end
+
+ it 'creates a commit with the correct parents' do
+ expect(merge_request_from_fork.source_branch_head.parents.map(&:id)).
+ to eq(['404fa3fc7c2c9b5dacff102f353bdf55b1be2813',
+ target_head])
+ end
+ end
+ end
+
+ context 'when a resolution is missing' do
+ let(:invalid_params) { { sections: { '2f6fcd96b88b36ce98c38da085c795a27d92a3dd_14_14' => 'head' } } }
+ let(:service) { MergeRequests::ResolveService.new(project, user, invalid_params) }
+
+ it 'raises a MissingResolution error' do
+ expect { service.execute(merge_request) }.
+ to raise_error(Gitlab::Conflict::File::MissingResolution)
+ end
+ end
+ end
+end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 00427d6db2a..3d854a959f3 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -330,13 +330,13 @@ describe SystemNoteService, services: true do
let(:mentioner) { project2.repository.commit }
it 'references the mentioning commit' do
- expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference(project)}"
+ expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference(project)}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
- expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference(project)}"
+ expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference(project)}"
end
end
end
@@ -346,13 +346,13 @@ describe SystemNoteService, services: true do
let(:mentioner) { project.repository.commit }
it 'references the mentioning commit' do
- expect(subject.note).to eq "mentioned in commit #{mentioner.to_reference}"
+ expect(subject.note).to eq "Mentioned in commit #{mentioner.to_reference}"
end
end
context 'from non-Commit' do
it 'references the mentioning object' do
- expect(subject.note).to eq "mentioned in issue #{mentioner.to_reference}"
+ expect(subject.note).to eq "Mentioned in issue #{mentioner.to_reference}"
end
end
end
@@ -362,7 +362,7 @@ describe SystemNoteService, services: true do
describe '.cross_reference?' do
it 'is truthy when text begins with expected text' do
- expect(described_class.cross_reference?('mentioned in something')).to be_truthy
+ expect(described_class.cross_reference?('Mentioned in something')).to be_truthy
end
it 'is falsey when text does not begin with expected text' do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 296fd1bd5a4..cafcad3e3c0 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -496,6 +496,7 @@ describe TodoService, services: true do
describe '#mark_todos_as_done' do
let(:issue) { create(:issue, project: project, author: author, assignee: john_doe) }
+ let(:another_issue) { create(:issue, project: project, author: author, assignee: john_doe) }
it 'marks a relation of todos as done' do
create(:todo, :mentioned, user: john_doe, target: issue, project: project)
@@ -518,6 +519,26 @@ describe TodoService, services: true do
expect(TodoService.new.mark_todos_as_done([todo], john_doe)).to eq(1)
end
+ context 'when some of the todos are done already' do
+ before do
+ create(:todo, :mentioned, user: john_doe, target: issue, project: project)
+ create(:todo, :mentioned, user: john_doe, target: another_issue, project: project)
+ end
+
+ it 'returns the number of those still pending' do
+ TodoService.new.mark_pending_todos_as_done(issue, john_doe)
+
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(1)
+ end
+
+ it 'returns 0 if all are done' do
+ TodoService.new.mark_pending_todos_as_done(issue, john_doe)
+ TodoService.new.mark_pending_todos_as_done(another_issue, john_doe)
+
+ expect(TodoService.new.mark_todos_as_done(Todo.all, john_doe)).to eq(0)
+ end
+ end
+
it 'caches the number of todos of a user', :caching do
create(:todo, :mentioned, user: john_doe, target: issue, project: project)
todo = create(:todo, :mentioned, user: john_doe, target: issue, project: project)
diff --git a/spec/simplecov_env.rb b/spec/simplecov_env.rb
index 6f8f7109e14..b507d38f472 100644
--- a/spec/simplecov_env.rb
+++ b/spec/simplecov_env.rb
@@ -1,4 +1,5 @@
require 'simplecov'
+require 'active_support/core_ext/numeric/time'
module SimpleCovEnv
extend self
@@ -48,7 +49,7 @@ module SimpleCovEnv
add_group 'Uploaders', 'app/uploaders'
add_group 'Validators', 'app/validators'
- merge_timeout 7200
+ merge_timeout 365.days
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 2e2aa7c4fc0..02b2b3ca101 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,13 +26,14 @@ RSpec.configure do |config|
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::TestHelpers, type: :controller
- config.include LoginHelpers, type: :feature
- config.include LoginHelpers, type: :request
+ config.include Devise::TestHelpers, type: :controller
+ config.include Warden::Test::Helpers, type: :request
+ config.include LoginHelpers, type: :feature
config.include StubConfiguration
config.include EmailHelpers
config.include TestEnv
config.include ActiveJob::TestHelper
+ config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb
index 927c72c7409..201614e45a4 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/taskable_shared_examples.rb
@@ -3,30 +3,57 @@
# Requires a context containing:
# subject { Issue or MergeRequest }
shared_examples 'a Taskable' do
- before do
- subject.description = <<-EOT.strip_heredoc
- * [ ] Task 1
- * [x] Task 2
- * [x] Task 3
- * [ ] Task 4
- * [ ] Task 5
- EOT
+ describe 'with multiple tasks' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ * [ ] Task 1
+ * [x] Task 2
+ * [x] Task 3
+ * [ ] Task 4
+ * [ ] Task 5
+ EOT
+ end
+
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('2 of')
+ expect(subject.task_status).to match('5 tasks completed')
+ end
+
+ describe '#tasks?' do
+ it 'returns true when object has tasks' do
+ expect(subject.tasks?).to eq true
+ end
+
+ it 'returns false when object has no tasks' do
+ subject.description = 'Now I have no tasks'
+ expect(subject.tasks?).to eq false
+ end
+ end
end
- it 'returns the correct task status' do
- expect(subject.task_status).to match('5 tasks')
- expect(subject.task_status).to match('2 completed')
- expect(subject.task_status).to match('3 remaining')
+ describe 'with an incomplete task' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ * [ ] Task 1
+ EOT
+ end
+
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('0 of')
+ expect(subject.task_status).to match('1 task completed')
+ end
end
- describe '#tasks?' do
- it 'returns true when object has tasks' do
- expect(subject.tasks?).to eq true
+ describe 'with a complete task' do
+ before do
+ subject.description = <<-EOT.strip_heredoc
+ * [x] Task 1
+ EOT
end
- it 'returns false when object has no tasks' do
- subject.description = 'Now I have no tasks'
- expect(subject.tasks?).to eq false
+ it 'returns the correct task status' do
+ expect(subject.task_status).to match('1 of')
+ expect(subject.task_status).to match('1 task completed')
end
end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index edbbfc3c9e5..0097dbf8fad 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -6,7 +6,7 @@ module TestEnv
# When developing the seed repository, comment out the branch you will modify.
BRANCH_SHA = {
'empty-branch' => '7efb185',
- 'ends-with.json' => '98b0d8b3',
+ 'ends-with.json' => '98b0d8b',
'flatten-dir' => 'e56497b',
'feature' => '0b4bc9a',
'feature_conflict' => 'bb5206f',
@@ -24,11 +24,12 @@ module TestEnv
'expand-collapse-lines' => '238e82d',
'video' => '8879059',
'crlf-diff' => '5938907',
- 'conflict-start' => '14fa46b',
+ 'conflict-start' => '75284c7',
'conflict-resolvable' => '1450cd6',
'conflict-binary-file' => '259a6fb',
'conflict-contains-conflict-markers' => '5e0964c',
'conflict-missing-side' => 'eb227b3',
+ 'conflict-non-utf8' => 'd0a293c',
'conflict-too-large' => '39fa04f',
}
@@ -36,9 +37,10 @@ module TestEnv
# need to keep all the branches in sync.
# We currently only need a subset of the branches
FORKED_BRANCH_SHA = {
- 'add-submodule-version-bump' => '3f547c08',
- 'master' => '5937ac0',
- 'remove-submodule' => '2a33e0c0'
+ 'add-submodule-version-bump' => '3f547c0',
+ 'master' => '5937ac0',
+ 'remove-submodule' => '2a33e0c',
+ 'conflict-resolvable-fork' => '404fa3f'
}
# Test environment
@@ -116,22 +118,7 @@ module TestEnv
system(*%W(#{Gitlab.config.git.bin_path} clone -q #{clone_url} #{repo_path}))
end
- Dir.chdir(repo_path) do
- branch_sha.each do |branch, sha|
- # Try to reset without fetching to avoid using the network.
- reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
- unless system(*reset)
- if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
- unless system(*reset)
- raise 'The fetched test seed '\
- 'does not contain the required revision.'
- end
- else
- raise 'Could not fetch test seed repository.'
- end
- end
- end
- end
+ set_repo_refs(repo_path, branch_sha)
# We must copy bare repositories because we will push to them.
system(git_env, *%W(#{Gitlab.config.git.bin_path} clone -q --bare #{repo_path} #{repo_path_bare}))
@@ -143,6 +130,7 @@ module TestEnv
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
+ set_repo_refs(target_repo_path, BRANCH_SHA)
end
def repos_path
@@ -159,6 +147,7 @@ module TestEnv
FileUtils.mkdir_p(target_repo_path)
FileUtils.cp_r("#{base_repo_path}/.", target_repo_path)
FileUtils.chmod_R 0755, target_repo_path
+ set_repo_refs(target_repo_path, FORKED_BRANCH_SHA)
end
# When no cached assets exist, manually hit the root path to create them
@@ -208,4 +197,23 @@ module TestEnv
def git_env
{ 'GIT_TEMPLATE_DIR' => '' }
end
+
+ def set_repo_refs(repo_path, branch_sha)
+ Dir.chdir(repo_path) do
+ branch_sha.each do |branch, sha|
+ # Try to reset without fetching to avoid using the network.
+ reset = %W(#{Gitlab.config.git.bin_path} update-ref refs/heads/#{branch} #{sha})
+ unless system(*reset)
+ if system(*%W(#{Gitlab.config.git.bin_path} fetch origin))
+ unless system(*reset)
+ raise 'The fetched test seed '\
+ 'does not contain the required revision.'
+ end
+ else
+ raise 'Could not fetch test seed repository.'
+ end
+ end
+ end
+ end
+ end
end
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
new file mode 100644
index 00000000000..31bbb150698
--- /dev/null
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -0,0 +1,53 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/edit.html.haml' do
+ include Devise::TestHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: fork_project,
+ target_project: project,
+ author: user)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:merge_request, closed_merge_request)
+
+ allow(view).to receive(:can?).and_return(true)
+ allow(view).to receive(:current_user)
+ .and_return(User.find(closed_merge_request.author_id))
+ end
+
+ context 'when a merge request without fork' do
+ it "shows editable fields" do
+ unlink_project.execute
+ closed_merge_request.reload
+
+ render
+
+ expect(rendered).to have_field('merge_request[title]')
+ expect(rendered).to have_field('merge_request[description]')
+ expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
+ expect(rendered).not_to have_selector('#merge_request_target_branch', visible: false)
+ end
+ end
+
+ context 'when a merge request with an existing source project is closed' do
+ it "shows editable fields" do
+ render
+
+ expect(rendered).to have_field('merge_request[title]')
+ expect(rendered).to have_field('merge_request[description]')
+ expect(rendered).to have_selector('#merge_request_assignee_id', visible: false)
+ expect(rendered).to have_selector('#merge_request_milestone_id', visible: false)
+ expect(rendered).to have_selector('#merge_request_target_branch', visible: false)
+ end
+ end
+end
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
new file mode 100644
index 00000000000..fe0780e72df
--- /dev/null
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'projects/merge_requests/show.html.haml' do
+ include Devise::TestHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+
+ let(:closed_merge_request) do
+ create(:closed_merge_request,
+ source_project: fork_project,
+ target_project: project,
+ author: user)
+ end
+
+ before do
+ assign(:project, project)
+ assign(:merge_request, closed_merge_request)
+ assign(:commits_count, 0)
+
+ allow(view).to receive(:can?).and_return(true)
+ end
+
+ context 'when the merge request is closed' do
+ it 'shows the "Reopen" button' do
+ render
+
+ expect(rendered).to have_css('a', visible: true, text: 'Reopen')
+ expect(rendered).to have_css('a', visible: false, text: 'Close')
+ end
+
+ it 'does not show the "Reopen" button when the source project does not exist' do
+ unlink_project.execute
+ closed_merge_request.reload
+
+ render
+
+ expect(rendered).to have_css('a', visible: false, text: 'Reopen')
+ expect(rendered).to have_css('a', visible: false, text: 'Close')
+ end
+ end
+end
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index eecc32875a5..7ca2c29da1c 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -2,19 +2,19 @@ require 'spec_helper'
describe EmailsOnPushWorker do
include RepoHelpers
+ include EmailSpec::Matchers
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
+ let(:email) { ActionMailer::Base.deliveries.last }
subject { EmailsOnPushWorker.new }
describe "#perform" do
context "when push is a new branch" do
- let(:email) { ActionMailer::Base.deliveries.last }
-
before do
data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA)
@@ -31,8 +31,6 @@ describe EmailsOnPushWorker do
end
context "when push is a deleted branch" do
- let(:email) { ActionMailer::Base.deliveries.last }
-
before do
data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA)
@@ -48,15 +46,40 @@ describe EmailsOnPushWorker do
end
end
- context "when there are no errors in sending" do
- let(:email) { ActionMailer::Base.deliveries.last }
+ context "when push is a force push to delete commits" do
+ before do
+ data_force_push = data.stringify_keys.merge(
+ "after" => data[:before],
+ "before" => data[:after]
+ )
+
+ subject.perform(project.id, recipients, data_force_push)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include('Change some files')
+ end
+ it "mentions force pushing in the body" do
+ expect(email).to have_body_text("force push")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when there are no errors in sending" do
before { perform }
it "sends a mail with the correct subject" do
expect(email.subject).to include('Change some files')
end
+ it "does not mention force pushing in the body" do
+ expect(email).not_to have_body_text("force push")
+ end
+
it "sends the mail to the correct recipient" do
expect(email.to).to eq([user.email])
end
@@ -66,6 +89,7 @@ describe EmailsOnPushWorker do
before do
ActionMailer::Base.deliveries.clear
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+ allow(subject).to receive_message_chain(:logger, :info)
perform
end
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
new file mode 100644
index 00000000000..689bc3d27b4
--- /dev/null
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe RemoveExpiredGroupLinksWorker do
+ describe '#perform' do
+ let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
+ let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
+ let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
+
+ it 'removes expired group links' do
+ expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
+ expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
+ end
+
+ it 'leaves group links that expire in the future' do
+ subject.perform
+ expect(project_group_link_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves group links that do not expire at all' do
+ subject.perform
+ expect(non_expiring_project_group_link.reload).to be_present
+ end
+ end
+end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
new file mode 100644
index 00000000000..402aa1e714e
--- /dev/null
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe RemoveExpiredMembersWorker do
+ let(:worker) { RemoveExpiredMembersWorker.new }
+
+ describe '#perform' do
+ context 'project members' do
+ let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_project_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(project_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_project_member.reload).to be_present
+ end
+ end
+
+ context 'group members' do
+ let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_group_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(group_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_group_member.reload).to be_present
+ end
+ end
+
+ context 'when the last group owner expires' do
+ let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) }
+
+ it 'does not delete the owner' do
+ worker.perform
+ expect(expired_group_owner.reload).to be_present
+ end
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js
index c264262ba73..c264262ba73 100755..100644
--- a/vendor/assets/javascripts/Chart.js
+++ b/vendor/assets/javascripts/Chart.js
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
index cfa49e72c50..cfa49e72c50 100755..100644
--- a/vendor/assets/javascripts/autosize.js
+++ b/vendor/assets/javascripts/autosize.js
diff --git a/vendor/assets/javascripts/jquery.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js
index 7ba17766b70..7ba17766b70 100755..100644
--- a/vendor/assets/javascripts/jquery.scrollTo.js
+++ b/vendor/assets/javascripts/jquery.scrollTo.js