summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml18
-rw-r--r--.gitlab/issue_templates/Bug.md2
-rw-r--r--CHANGELOG63
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/images/icon-link.pngbin729 -> 0 bytes
-rw-r--r--app/assets/images/icon_anchor.svg1
-rw-r--r--app/assets/javascripts/LabelManager.js5
-rw-r--r--app/assets/javascripts/api.js6
-rw-r--r--app/assets/javascripts/application.js64
-rw-r--r--app/assets/javascripts/autosave.js6
-rw-r--r--app/assets/javascripts/awards_handler.js15
-rw-r--r--app/assets/javascripts/behaviors/autosize.js2
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js6
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js20
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js19
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js7
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js3
-rw-r--r--app/assets/javascripts/blob/template_selector.js3
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js2
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es69
-rw-r--r--app/assets/javascripts/breakpoints.js2
-rw-r--r--app/assets/javascripts/build.js12
-rw-r--r--app/assets/javascripts/build_variables.js.es66
-rw-r--r--app/assets/javascripts/commit/image-file.js2
-rw-r--r--app/assets/javascripts/commits.js1
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js7
-rw-r--r--app/assets/javascripts/diff.js3
-rw-r--r--app/assets/javascripts/dispatcher.js9
-rw-r--r--app/assets/javascripts/due_date_select.js3
-rw-r--r--app/assets/javascripts/extensions/jquery.js2
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es624
-rw-r--r--app/assets/javascripts/gl_dropdown.js64
-rw-r--r--app/assets/javascripts/gl_form.js6
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js8
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js1
-rw-r--r--app/assets/javascripts/groups_select.js1
-rw-r--r--app/assets/javascripts/importer_status.js15
-rw-r--r--app/assets/javascripts/issuable.js.es6 (renamed from app/assets/javascripts/issuable.js)37
-rw-r--r--app/assets/javascripts/issue.js9
-rw-r--r--app/assets/javascripts/issues-bulk-assignment.js10
-rw-r--r--app/assets/javascripts/labels.js3
-rw-r--r--app/assets/javascripts/labels_select.js9
-rw-r--r--app/assets/javascripts/lib/chart.js1
-rw-r--r--app/assets/javascripts/lib/cropper.js1
-rw-r--r--app/assets/javascripts/lib/d3.js1
-rw-r--r--app/assets/javascripts/lib/raphael.js5
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js1
-rw-r--r--app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb2
-rw-r--r--app/assets/javascripts/lib/utils/emoji_aliases.js.erb6
-rw-r--r--app/assets/javascripts/lib/utils/notify.js5
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js5
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js6
-rw-r--r--app/assets/javascripts/line_highlighter.js69
-rw-r--r--app/assets/javascripts/merge_request.js15
-rw-r--r--app/assets/javascripts/merge_request_tabs.js84
-rw-r--r--app/assets/javascripts/merge_request_widget.js8
-rw-r--r--app/assets/javascripts/milestone.js1
-rw-r--r--app/assets/javascripts/milestone_select.js1
-rw-r--r--app/assets/javascripts/network/branch-graph.js13
-rw-r--r--app/assets/javascripts/network/network_bundle.js7
-rw-r--r--app/assets/javascripts/notes.js81
-rw-r--r--app/assets/javascripts/preview_markdown.js10
-rw-r--r--app/assets/javascripts/profile/gl_crop.js12
-rw-r--r--app/assets/javascripts/profile/profile.js4
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js1
-rw-r--r--app/assets/javascripts/project.js6
-rw-r--r--app/assets/javascripts/project_find_file.js9
-rw-r--r--app/assets/javascripts/project_new.js20
-rw-r--r--app/assets/javascripts/project_show.js2
-rw-r--r--app/assets/javascripts/projects_list.js1
-rw-r--r--app/assets/javascripts/protected_branch_dropdown.js.es61
-rw-r--r--app/assets/javascripts/search_autocomplete.js22
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js2
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js6
-rw-r--r--app/assets/javascripts/sidebar.js41
-rw-r--r--app/assets/javascripts/sidebar.js.es693
-rw-r--r--app/assets/javascripts/snippets_list.js.es611
-rw-r--r--app/assets/javascripts/syntax_highlight.js11
-rw-r--r--app/assets/javascripts/todos.js6
-rw-r--r--app/assets/javascripts/tree.js3
-rw-r--r--app/assets/javascripts/u2f/authenticate.js18
-rw-r--r--app/assets/javascripts/u2f/register.js7
-rw-r--r--app/assets/javascripts/user_tabs.js69
-rw-r--r--app/assets/javascripts/users/calendar.js23
-rw-r--r--app/assets/javascripts/users/users_bundle.js1
-rw-r--r--app/assets/javascripts/users_select.js8
-rw-r--r--app/assets/javascripts/zen_mode.js37
-rw-r--r--app/assets/stylesheets/framework/animations.scss67
-rw-r--r--app/assets/stylesheets/framework/blocks.scss4
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/files.scss24
-rw-r--r--app/assets/stylesheets/framework/filters.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss4
-rw-r--r--app/assets/stylesheets/framework/mixins.scss12
-rw-r--r--app/assets/stylesheets/framework/nav.scss2
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss18
-rw-r--r--app/assets/stylesheets/framework/tw_bootstrap_variables.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss27
-rw-r--r--app/assets/stylesheets/framework/variables.scss140
-rw-r--r--app/assets/stylesheets/pages/awards.scss7
-rw-r--r--app/assets/stylesheets/pages/builds.scss28
-rw-r--r--app/assets/stylesheets/pages/commits.scss12
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss15
-rw-r--r--app/assets/stylesheets/pages/issues.scss15
-rw-r--r--app/assets/stylesheets/pages/labels.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_conflicts.scss2
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss12
-rw-r--r--app/assets/stylesheets/pages/todos.scss2
-rw-r--r--app/assets/stylesheets/pages/tree.scss31
-rw-r--r--app/assets/stylesheets/pages/xterm.scss3
-rw-r--r--app/controllers/ci/lints_controller.rb11
-rw-r--r--app/controllers/concerns/issuable_actions.rb35
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/import/base_controller.rb17
-rw-r--r--app/controllers/import/bitbucket_controller.rb23
-rw-r--r--app/controllers/import/github_controller.rb13
-rw-r--r--app/controllers/import/gitlab_controller.rb15
-rw-r--r--app/controllers/projects/builds_controller.rb4
-rw-r--r--app/controllers/projects/git_http_client_controller.rb4
-rw-r--r--app/controllers/projects/git_http_controller.rb3
-rw-r--r--app/controllers/projects/hooks_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb26
-rw-r--r--app/controllers/projects/lfs_storage_controller.rb11
-rw-r--r--app/controllers/projects/merge_requests_controller.rb31
-rw-r--r--app/controllers/projects/pipelines_controller.rb9
-rw-r--r--app/controllers/projects/services_controller.rb5
-rw-r--r--app/finders/pipelines_finder.rb32
-rw-r--r--app/helpers/avatars_helper.rb3
-rw-r--r--app/helpers/git_helper.rb4
-rw-r--r--app/helpers/import_helper.rb5
-rw-r--r--app/helpers/merge_requests_helper.rb10
-rw-r--r--app/helpers/nav_helper.rb19
-rw-r--r--app/helpers/projects_helper.rb24
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/helpers/services_helper.rb6
-rw-r--r--app/helpers/sidekiq_helper.rb19
-rw-r--r--app/helpers/workhorse_helper.rb4
-rw-r--r--app/models/ci/build.rb27
-rw-r--r--app/models/ci/pipeline.rb11
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/diff_note.rb18
-rw-r--r--app/models/discussion.rb39
-rw-r--r--app/models/hooks/project_hook.rb1
-rw-r--r--app/models/hooks/web_hook.rb1
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/slack_service.rb72
-rw-r--r--app/models/project_services/slack_service/build_message.rb4
-rw-r--r--app/models/project_services/slack_service/pipeline_message.rb79
-rw-r--r--app/models/repository.rb63
-rw-r--r--app/models/service.rb4
-rw-r--r--app/services/issuable/bulk_update_service.rb26
-rw-r--r--app/services/issues/base_service.rb7
-rw-r--r--app/services/issues/bulk_update_service.rb25
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/todo_service.rb20
-rw-r--r--app/views/admin/background_jobs/show.html.haml8
-rw-r--r--app/views/groups/group_members/update.js.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml547
-rw-r--r--app/views/import/base/create.js.haml21
-rw-r--r--app/views/import/base/unauthorized.js.haml14
-rw-r--r--app/views/import/bitbucket/deploy_key.js.haml3
-rw-r--r--app/views/import/bitbucket/status.html.haml2
-rw-r--r--app/views/import/github/status.html.haml2
-rw-r--r--app/views/import/gitlab/status.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/projects/_merge_request_settings.html.haml25
-rw-r--r--app/views/projects/blame/show.html.haml3
-rw-r--r--app/views/projects/builds/_sidebar.html.haml80
-rw-r--r--app/views/projects/buttons/_fork.html.haml4
-rw-r--r--app/views/projects/forks/index.html.haml4
-rw-r--r--app/views/projects/hooks/_project_hook.html.haml2
-rw-r--r--app/views/projects/issues/_issue.html.haml4
-rw-r--r--app/views/projects/issues/_issues.html.haml2
-rw-r--r--app/views/projects/issues/_new_branch.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/_merge_request.html.haml4
-rw-r--r--app/views/projects/merge_requests/_merge_requests.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml2
-rw-r--r--app/views/projects/merge_requests/index.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_how_to_merge.html.haml7
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml69
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/project_members/update.js.haml2
-rw-r--r--app/views/projects/refs/logs_tree.js.haml4
-rw-r--r--app/views/projects/tree/_blob_item.html.haml6
-rw-r--r--app/views/projects/tree/_tree_content.html.haml14
-rw-r--r--app/views/projects/tree/_tree_item.html.haml6
-rw-r--r--app/views/projects/triggers/index.html.haml130
-rw-r--r--app/views/shared/issuable/_filter.html.haml17
-rw-r--r--app/views/shared/web_hooks/_form.html.haml7
-rw-r--r--app/views/snippets/_snippets.html.haml16
-rw-r--r--app/workers/prune_old_events_worker.rb17
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/gitlab_workhorse_secret.rb8
-rw-r--r--config/routes.rb1
-rw-r--r--db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb15
-rw-r--r--db/migrate/20160830211132_add_confidential_issues_events_to_services.rb15
-rw-r--r--db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb15
-rw-r--r--db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb39
-rw-r--r--db/schema.rb50
-rw-r--r--doc/api/README.md6
-rw-r--r--doc/api/ci/lint.md49
-rw-r--r--doc/api/members.md4
-rw-r--r--doc/api/projects.md3
-rw-r--r--doc/ci/examples/README.md14
-rw-r--r--doc/ci/ssh_keys/README.md3
-rw-r--r--doc/ci/triggers/img/builds_page.pngbin33324 -> 76181 bytes
-rw-r--r--doc/ci/triggers/img/trigger_single_build.pngbin2387 -> 21152 bytes
-rw-r--r--doc/ci/triggers/img/trigger_variables.pngbin4433 -> 9315 bytes
-rw-r--r--doc/ci/triggers/img/triggers_page.pngbin12943 -> 12002 bytes
-rw-r--r--doc/ci/variables/README.md4
-rw-r--r--doc/ci/yaml/README.md55
-rw-r--r--doc/gitlab-basics/add-file.md4
-rw-r--r--doc/install/installation.md2
-rw-r--r--doc/install/requirements.md2
-rw-r--r--doc/intro/README.md6
-rw-r--r--doc/update/8.11-to-8.12.md2
-rw-r--r--doc/user/project/merge_requests.md166
-rw-r--r--doc/user/project/merge_requests/authorization_for_merge_requests.md56
-rw-r--r--doc/user/project/merge_requests/cherry_pick_changes.md52
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_commit.png (renamed from doc/workflow/img/cherry_pick_changes_commit.png)bin304098 -> 304098 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png (renamed from doc/workflow/img/cherry_pick_changes_commit_modal.png)bin264883 -> 264883 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_mr.png (renamed from doc/workflow/img/cherry_pick_changes_mr.png)bin212267 -> 212267 bytes
-rw-r--r--doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png (renamed from doc/workflow/img/cherry_pick_changes_mr_modal.png)bin186597 -> 186597 bytes
-rw-r--r--doc/user/project/merge_requests/img/commit_compare.png (renamed from doc/workflow/merge_requests/commit_compare.png)bin65010 -> 65010 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_request_diff.pngbin0 -> 69394 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png (renamed from doc/workflow/merge_when_build_succeeds/enable.png)bin68769 -> 68769 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.pngbin0 -> 11136 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png (renamed from doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png)bin17552 -> 17552 bytes
-rw-r--r--doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png (renamed from doc/workflow/merge_when_build_succeeds/status.png)bin82655 -> 82655 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_commit.png (renamed from doc/workflow/img/revert_changes_commit.png)bin233750 -> 233750 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_commit_modal.png (renamed from doc/workflow/img/revert_changes_commit_modal.png)bin205046 -> 205046 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_mr.png (renamed from doc/workflow/img/revert_changes_mr.png)bin241051 -> 241051 bytes
-rw-r--r--doc/user/project/merge_requests/img/revert_changes_mr_modal.png (renamed from doc/workflow/img/revert_changes_mr_modal.png)bin211022 -> 211022 bytes
-rw-r--r--doc/user/project/merge_requests/img/versions.pngbin0 -> 35001 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_blocked_accept_button.png (renamed from doc/workflow/wip_merge_requests/blocked_accept_button.png)bin32720 -> 32720 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_mark_as_wip.png (renamed from doc/workflow/wip_merge_requests/mark_as_wip.png)bin21640 -> 21640 bytes
-rw-r--r--doc/user/project/merge_requests/img/wip_unmark_as_wip.png (renamed from doc/workflow/wip_merge_requests/unmark_as_wip.png)bin16606 -> 16606 bytes
-rw-r--r--doc/user/project/merge_requests/merge_when_build_succeeds.md46
-rw-r--r--doc/user/project/merge_requests/revert_changes.md64
-rw-r--r--doc/user/project/merge_requests/versions.md28
-rw-r--r--doc/user/project/merge_requests/work_in_progress_merge_requests.md17
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_dropdown.png (renamed from doc/workflow/img/web_editor_new_branch_dropdown.png)bin20436 -> 20436 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_page.png (renamed from doc/workflow/img/web_editor_new_branch_page.png)bin11245 -> 11245 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_directory_dialog.png (renamed from doc/workflow/img/web_editor_new_directory_dialog.png)bin13339 -> 13339 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_directory_dropdown.png (renamed from doc/workflow/img/web_editor_new_directory_dropdown.png)bin20007 -> 20007 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_file_dropdown.png (renamed from doc/workflow/img/web_editor_new_file_dropdown.png)bin20680 -> 20680 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_file_editor.png (renamed from doc/workflow/img/web_editor_new_file_editor.png)bin66261 -> 66261 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_push_widget.png (renamed from doc/workflow/img/web_editor_new_push_widget.png)bin7076 -> 7076 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_tag_dropdown.png (renamed from doc/workflow/img/web_editor_new_tag_dropdown.png)bin20080 -> 20080 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_new_tag_page.png (renamed from doc/workflow/img/web_editor_new_tag_page.png)bin36610 -> 36610 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_start_new_merge_request.png (renamed from doc/workflow/img/web_editor_start_new_merge_request.png)bin8596 -> 8596 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_buttons.pngbin0 -> 14131 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_first_file.pngbin0 -> 25748 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_template_dropdown_mit_license.pngbin0 -> 85413 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_upload_file_dialog.png (renamed from doc/workflow/img/web_editor_upload_file_dialog.png)bin21502 -> 21502 bytes
-rw-r--r--doc/user/project/repository/img/web_editor_upload_file_dropdown.png (renamed from doc/workflow/img/web_editor_upload_file_dropdown.png)bin20651 -> 20651 bytes
-rw-r--r--doc/user/project/repository/web_editor.md175
-rw-r--r--doc/workflow/README.md17
-rw-r--r--doc/workflow/authorization_for_merge_requests.md41
-rw-r--r--doc/workflow/cherry_pick_changes.md53
-rw-r--r--doc/workflow/merge_requests.md91
-rw-r--r--doc/workflow/merge_requests/merge_request_diff.pngbin103239 -> 0 bytes
-rw-r--r--doc/workflow/merge_requests/merge_request_diff_without_whitespace.pngbin71896 -> 0 bytes
-rw-r--r--doc/workflow/merge_requests/versions.pngbin100566 -> 0 bytes
-rw-r--r--doc/workflow/merge_when_build_succeeds.md16
-rw-r--r--doc/workflow/revert_changes.md65
-rw-r--r--doc/workflow/web_editor.md152
-rw-r--r--doc/workflow/wip_merge_requests.md14
-rw-r--r--features/steps/admin/settings.rb1
-rw-r--r--features/steps/project/merge_requests.rb4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/internal.rb14
-rw-r--r--lib/api/lint.rb21
-rw-r--r--lib/api/pipelines.rb5
-rw-r--r--lib/api/projects.rb18
-rw-r--r--lib/ci/api/builds.rb6
-rw-r--r--lib/ci/api/helpers.rb8
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb26
-rw-r--r--lib/gitlab/bitbucket_import/importer.rb4
-rw-r--r--lib/gitlab/ci/config.rb2
-rw-r--r--lib/gitlab/ci/config/node/configurable.rb10
-rw-r--r--lib/gitlab/ci/config/node/entry.rb14
-rw-r--r--lib/gitlab/ci/config/node/factory.rb8
-rw-r--r--lib/gitlab/ci/config/node/global.rb14
-rw-r--r--lib/gitlab/ci/config/node/job.rb42
-rw-r--r--lib/gitlab/ci/config/node/jobs.rb28
-rw-r--r--lib/gitlab/ci/config/node/null.rb34
-rw-r--r--lib/gitlab/ci/config/node/undefined.rb27
-rw-r--r--lib/gitlab/ci/config/node/unspecified.rb19
-rw-r--r--lib/gitlab/ci/pipeline_duration.rb141
-rw-r--r--lib/gitlab/conflict/parser.rb2
-rw-r--r--lib/gitlab/github_import/base_formatter.rb2
-rw-r--r--lib/gitlab/github_import/comment_formatter.rb2
-rw-r--r--lib/gitlab/github_import/importer.rb3
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb4
-rw-r--r--lib/gitlab/github_import/label_formatter.rb6
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb4
-rw-r--r--lib/gitlab/gitlab_import/importer.rb5
-rw-r--r--lib/gitlab/popen.rb4
-rw-r--r--lib/gitlab/project_search_results.rb5
-rw-r--r--lib/gitlab/search_results.rb9
-rw-r--r--lib/gitlab/snippet_search_results.rb4
-rw-r--r--lib/gitlab/workhorse.rb52
-rw-r--r--spec/controllers/import/bitbucket_controller_spec.rb41
-rw-r--r--spec/controllers/import/github_controller_spec.rb41
-rw-r--r--spec/controllers/import/gitlab_controller_spec.rb41
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb6
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb6
-rw-r--r--spec/controllers/projects/services_controller_spec.rb16
-rw-r--r--spec/factories/notes.rb5
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb7
-rw-r--r--spec/features/issues/reset_filters_spec.rb81
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb15
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb43
-rw-r--r--spec/features/merge_requests/update_merge_requests_spec.rb132
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb10
-rw-r--r--spec/features/projects/builds_spec.rb134
-rw-r--r--spec/features/projects/edit_spec.rb57
-rw-r--r--spec/features/triggers_spec.rb2
-rw-r--r--spec/features/users/snippets_spec.rb29
-rw-r--r--spec/finders/pipelines_finder_spec.rb51
-rw-r--r--spec/helpers/git_helper_spec.rb9
-rw-r--r--spec/helpers/import_helper_spec.rb24
-rw-r--r--spec/helpers/nav_helper_spec.rb25
-rw-r--r--spec/helpers/projects_helper_spec.rb44
-rw-r--r--spec/helpers/search_helper_spec.rb4
-rw-r--r--spec/helpers/sidekiq_helper_spec.rb40
-rw-r--r--spec/javascripts/awards_handler_spec.js7
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js3
-rw-r--r--spec/javascripts/datetime_utility_spec.js.coffee50
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es664
-rw-r--r--spec/javascripts/fixtures/comments.html.haml21
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js8
-rw-r--r--spec/javascripts/issue_spec.js2
-rw-r--r--spec/javascripts/new_branch_spec.js2
-rw-r--r--spec/javascripts/notes_spec.js57
-rw-r--r--spec/javascripts/project_title_spec.js12
-rw-r--r--spec/javascripts/right_sidebar_spec.js4
-rw-r--r--spec/javascripts/search_autocomplete_spec.js14
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js1
-rw-r--r--spec/javascripts/spec_helper.js38
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js8
-rw-r--r--spec/javascripts/u2f/register_spec.js8
-rw-r--r--spec/javascripts/zen_mode_spec.js4
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb35
-rw-r--r--spec/lib/gitlab/ci/config/node/cache_spec.rb2
-rw-r--r--spec/lib/gitlab/ci/config/node/factory_spec.rb3
-rw-r--r--spec/lib/gitlab/ci/config/node/global_spec.rb81
-rw-r--r--spec/lib/gitlab/ci/config/node/job_spec.rb88
-rw-r--r--spec/lib/gitlab/ci/config/node/jobs_spec.rb8
-rw-r--r--spec/lib/gitlab/ci/config/node/null_spec.rb41
-rw-r--r--spec/lib/gitlab/ci/config/node/script_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/config/node/undefined_spec.rb33
-rw-r--r--spec/lib/gitlab/ci/config/node/unspecified_spec.rb32
-rw-r--r--spec/lib/gitlab/ci/pipeline_duration_spec.rb115
-rw-r--r--spec/lib/gitlab/conflict/parser_spec.rb4
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/label_formatter_spec.rb28
-rw-r--r--spec/lib/gitlab/gitlab_import/importer_spec.rb2
-rw-r--r--spec/lib/gitlab/popen_spec.rb9
-rw-r--r--spec/lib/gitlab/search_results_spec.rb18
-rw-r--r--spec/lib/gitlab/snippet_search_results_spec.rb6
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb86
-rw-r--r--spec/models/ci/build_spec.rb60
-rw-r--r--spec/models/ci/pipeline_spec.rb35
-rw-r--r--spec/models/diff_note_spec.rb37
-rw-r--r--spec/models/discussion_spec.rb96
-rw-r--r--spec/models/project_services/slack_service/build_message_spec.rb32
-rw-r--r--spec/models/project_services/slack_service/pipeline_message_spec.rb55
-rw-r--r--spec/models/project_services/slack_service_spec.rb71
-rw-r--r--spec/models/repository_spec.rb65
-rw-r--r--spec/requests/api/fork_spec.rb66
-rw-r--r--spec/requests/api/internal_spec.rb4
-rw-r--r--spec/requests/api/lint_spec.rb49
-rw-r--r--spec/requests/ci/api/builds_spec.rb11
-rw-r--r--spec/requests/git_http_spec.rb18
-rw-r--r--spec/requests/lfs_http_spec.rb19
-rw-r--r--spec/services/issuable/bulk_update_service_spec.rb (renamed from spec/services/issues/bulk_update_service_spec.rb)6
-rw-r--r--spec/services/issues/close_service_spec.rb40
-rw-r--r--spec/services/issues/create_service_spec.rb18
-rw-r--r--spec/services/issues/reopen_service_spec.rb46
-rw-r--r--spec/services/issues/update_service_spec.rb93
-rw-r--r--spec/services/merge_requests/build_service_spec.rb4
-rw-r--r--spec/services/todo_service_spec.rb16
-rw-r--r--spec/support/db_cleaner.rb2
-rw-r--r--spec/support/issuable_slash_commands_shared_examples.rb68
-rw-r--r--spec/support/login_helpers.rb1
-rw-r--r--spec/support/slash_commands_helpers.rb10
-rw-r--r--spec/support/workhorse_helpers.rb5
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb12
-rw-r--r--spec/workers/prune_old_events_worker_spec.rb24
-rw-r--r--vendor/assets/javascripts/task_list.js161
403 files changed, 5871 insertions, 2398 deletions
diff --git a/.gitignore b/.gitignore
index 1bf9a47aef6..9166512606d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -48,3 +48,4 @@
/vendor/bundle/*
/builds/*
/shared/*
+/.gitlab_workhorse_secret
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index be5614520a5..f51d506f64a 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -82,7 +82,7 @@ update-knapsack:
- export KNAPSACK_REPORT_PATH=knapsack/rspec_node_${CI_NODE_INDEX}_${CI_NODE_TOTAL}_report.json
- export KNAPSACK_GENERATE_REPORT=true
- cp knapsack/rspec_report.json ${KNAPSACK_REPORT_PATH}
- - knapsack rspec
+ - knapsack rspec "--color --format documentation"
artifacts:
expire_in: 31d
paths:
@@ -248,6 +248,21 @@ bundler:audit:
script:
- "bundle exec bundle-audit check --update --ignore OSVDB-115941"
+migration paths:
+ stage: test
+ <<: *use-db
+ only:
+ - master@gitlab-org/gitlab-ce
+ script:
+ - git checkout HEAD .
+ - git fetch --tags
+ - git checkout v8.5.9
+ - 'echo test: unix:/var/opt/gitlab/redis/redis.socket > config/resque.yml'
+ - bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}" --retry=3
+ - rake db:drop db:create db:schema:load db:seed_fu
+ - git checkout $CI_BUILD_REF
+ - rake db:migrate
+
coverage:
stage: post-test
services: []
@@ -263,7 +278,6 @@ coverage:
- coverage/index.html
- coverage/assets/
-
# Notify slack in the end
notify:slack:
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index b676916fdf4..ac38f0c9521 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -10,7 +10,7 @@
(What you should see instead)
-### Actual behaviour
+### Actual behavior
(What actually happens)
diff --git a/CHANGELOG b/CHANGELOG
index e91bc0b9b17..57b4d7aecf1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,26 +1,45 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.12.0 (unreleased)
+ - Update the rouge gem to 2.0.6, which adds highlighting support for JSX, Prometheus, and others. !6251
+ - Add ability to fork to a specific namespace using API. (ritave)
+ - Cleanup misalignments in Issue list view !6206
+ - Prune events older than 12 months. (ritave)
+ - Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
- Filter tags by name !6121
+ - Give project selection dropdowns responsive width, make non-wrapping.
- 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)
- Instructions for enabling Git packfile bitmaps !6104
+ - Fix pagination on user snippets page
+ - Escape search term before passing it to Regexp.new !6241 (winniehell)
+ - Fix pinned sidebar behavior in smaller viewports !6169
- 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)
+ - Move parsing of sidekiq ps into helper !6245 (pascalbetz)
- Expose `sha` and `merge_commit_sha` in merge request API (Ben Boeckel)
- Set path for all JavaScript cookies to honor GitLab's subdirectory setting !5627 (Mike Greiling)
+ - Fix blame table layout width
- Fix bug where pagination is still displayed despite all todos marked as done (ClemMakesApps)
- Center build stage columns in pipeline overview (ClemMakesApps)
+ - Rename behaviour to behavior in bug issue template for consistency (ClemMakesApps)
+ - Remove suggested colors hover underline (ClemMakesApps)
- Shorten task status phrase (ClemMakesApps)
+ - Fix project visibility level fields on settings
- Add hover color to emoji icon (ClemMakesApps)
+ - Add textarea autoresize after comment (ClemMakesApps)
+ - Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions
- Add white background for no readme container (ClemMakesApps)
- API: Expose issue confidentiality flag. (Robert Schilling)
+ - Fix markdown anchor icon interaction (ClemMakesApps)
+ - Test migration paths from 8.5 until current release !4874
+ - Replace animateEmoji timeout with eventListener (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
@@ -28,27 +47,40 @@ v 8.12.0 (unreleased)
- Remove prefixes from transition CSS property (ClemMakesApps)
- Add Sentry logging to API calls
- Add BroadcastMessage API
+ - Use 'git update-ref' for safer web commits !6130
+ - Sort pipelines requested through the API
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+ - Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084
+ - Show queued time when showing a pipeline !6084
- 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)
- Use JavaScript tooltips for mentions !5301 (winniehell)
+ - Add hover state to todos !5361 (winniehell)
+ - Fix icon alignment of star and fork buttons !5451 (winniehell)
+ - Fix alignment of icon buttons !5887 (winniehell)
- Fix markdown help references (ClemMakesApps)
- Add last commit time to repo view (ClemMakesApps)
- Fix accessibility and visibility of project list dropdown button !6140
+ - Fix missing flash messages on service edit page (airatshigapov)
- Added project specific enable/disable setting for LFS !5997
- Don't expose a user's token in the `/api/v3/user` API (!6047)
- Remove redundant js-timeago-pending from user activity log (ClemMakesApps)
- Ability to manage project issues, snippets, wiki, merge requests and builds access level
- Remove inconsistent font weight for sidebar's labels (ClemMakesApps)
+ - Align add button on repository view (ClemMakesApps)
+ - Fix contributions calendar month label truncation (ClemMakesApps)
- Added tests for diff notes
+ - Add pipeline events to Slack integration !5525
- Add a button to download latest successful artifacts for branches and tags !5142
- Remove redundant pipeline tooltips (ClemMakesApps)
- Expire commit info views after one day, instead of two weeks, to allow for user email updates
- Add delimiter to project stars and forks count (ClemMakesApps)
- Fix badge count alignment (ClemMakesApps)
+ - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
- Fix repo title alignment (ClemMakesApps)
+ - Change update interval of contacted_at
- 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)
@@ -59,22 +91,42 @@ v 8.12.0 (unreleased)
- 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.
+ - Convert datetime coffeescript spec to ES6 (ClemMakesApps)
- Add merge request versions !5467
- Change using size to use count and caching it for number of group members. !5935
- Replace play icon font with svg (ClemMakesApps)
- Added 'only_allow_merge_if_build_succeeds' project setting in the API. !5930 (Duck)
- Reduce number of database queries on builds tab
+ - Wrap text in commit message containers
- Capitalize mentioned issue timeline notes (ClemMakesApps)
- Fix inconsistent checkbox alignment (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
+ - Avoid conflict with admin labels when importing GitHub labels
- User can edit closed MR with deleted fork (Katarzyna Kobierska Ula Budziszewska) !5496
+ - Fix repository page ui issues
- Fixed invisible scroll controls on build page on iPhone
-
-v 8.11.5 (unreleased)
- - Optimize branch lookups and force a repository reload for Repository#find_branch
+ - Fix error on raw build trace download for old builds stored in database !4822
+ - Refactor the triggers page and documentation !6217
+ - Show values of CI trigger variables only when clicked (Katarzyna Kobierska Ula Budziszewska)
+ - Use default clone protocol on "check out, review, and merge locally" help page URL
+ - API for Ci Lint !5953 (Katarzyna Kobierska Urszula Budziszewska)
+ - Allow bulk update merge requests from merge requests index page
+
+v 8.11.6 (unreleased)
+
+v 8.11.5
+ - Optimize branch lookups and force a repository reload for Repository#find_branch. !6087
+ - Fix member expiration date picker after update. !6184
- Fix suggested colors options for new labels in the admin area. !6138
+ - Optimize discussion notes resolving and unresolving
+ - Fix GitLab import button
+ - Fix confidential issues being exposed as public using gitlab.com export
+ - Remove gitorious from import_sources. !6180
+ - Scope webhooks/services that will run for confidential issues
+ - Remove gitorious from import_sources
+ - Fix confidential issues being exposed as public using gitlab.com export
v 8.11.4
- Fix resolving conflicts on forks. !6082
@@ -91,6 +143,7 @@ v 8.11.4
- Fix issue boards leak private label names and descriptions
- Fix broken gitlab:backup:restore because of bad permissions on repo storage !6098 (Dirk Hörner)
- Remove gitorious. !5866
+ - Allow compare merge request versions
v 8.11.3
- Allow system info page to handle case where info is unavailable
@@ -101,6 +154,7 @@ v 8.11.3
- Fix external issue tracker "Issues" link leading to 404s
- Don't try to show merge conflict resolution info if a merge conflict contains non-UTF-8 characters
- Automatically expand hidden discussions when accessed by a permalink !5585 (Mike Greiling)
+ - Issues filters reset button
v 8.11.2
- Show "Create Merge Request" widget for push events to fork projects on the source project. !5978
@@ -127,7 +181,6 @@ v 8.11.0
- Add Issues Board !5548
- Allow resolving merge conflicts in the UI !5479
- Improve diff performance by eliminating redundant checks for text blobs
- - Remove green outline from `New branch unavailable` button on issue page !5858 (winniehell)
- Ensure that branch names containing escapable characters (e.g. %20) aren't unescaped indiscriminately. !5770 (ewiltshi)
- Convert switch icon into icon font (ClemMakesApps)
- API: Endpoints for enabling and disabling deploy keys
@@ -170,8 +223,6 @@ v 8.11.0
- Update `timeago` plugin to use multiple string/locale settings
- Remove unused images (ClemMakesApps)
- Get issue and merge request description templates from repositories
- - Add hover state to todos !5361 (winniehell)
- - Fix icon alignment of star and fork buttons !5451 (winniehell)
- 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
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index b4d6d12101f..6f4eebdf6f6 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.7.11
+0.8.1
diff --git a/Gemfile b/Gemfile
index 620338e5997..81b7002027a 100644
--- a/Gemfile
+++ b/Gemfile
@@ -53,7 +53,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.5'
+gem 'gitlab_git', '~> 10.6.3'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
diff --git a/Gemfile.lock b/Gemfile.lock
index 28ede86b3ba..c421713f6a1 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -279,7 +279,7 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.5.0)
+ gitlab_git (10.6.3)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -584,7 +584,7 @@ GEM
railties (>= 4.2.0, < 5.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (2.0.5)
+ rouge (2.0.6)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
@@ -858,7 +858,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.5)
+ gitlab_git (~> 10.6.3)
gitlab_meta (= 7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
diff --git a/app/assets/images/icon-link.png b/app/assets/images/icon-link.png
deleted file mode 100644
index 5b55e12571c..00000000000
--- a/app/assets/images/icon-link.png
+++ /dev/null
Binary files differ
diff --git a/app/assets/images/icon_anchor.svg b/app/assets/images/icon_anchor.svg
new file mode 100644
index 00000000000..7e242586bad
--- /dev/null
+++ b/app/assets/images/icon_anchor.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><path fill="#333" fill-rule="evenodd" d="M9.683 6.676l-.047-.048C8.27 5.26 6.07 5.243 4.726 6.588l-2.29 2.29c-1.344 1.344-1.328 3.544.04 4.91 1.366 1.368 3.564 1.385 4.908.04l1.753-1.752c-.695.074-1.457-.078-2.176-.444L5.934 12.66c-.634.634-1.67.625-2.312-.017-.642-.643-.65-1.677-.017-2.312L6.035 7.9c.634-.634 1.67-.625 2.312.017.024.024.048.05.07.075l.003-.002c.36.36.943.366 1.3.01.355-.356.35-.938-.01-1.3l-.027-.024zM6.58 9.586l.048.05c1.367 1.366 3.565 1.384 4.91.04l2.29-2.292c1.344-1.343 1.328-3.542-.04-4.91-1.366-1.366-3.564-1.384-4.908-.04L7.127 4.187c.695-.074 1.457.078 2.176.444l1.028-1.027c.635-.634 1.67-.624 2.313.017.643.644.652 1.678.018 2.312l-2.43 2.432c-.635.634-1.67.624-2.313-.018-.024-.024-.048-.05-.07-.075l-.003.004c-.36-.362-.943-.367-1.3-.01-.355.355-.35.937.01 1.3.01.007.018.015.027.023z"/></svg> \ No newline at end of file
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js
index 151455ce4a3..d4a4c7abaa1 100644
--- a/app/assets/javascripts/LabelManager.js
+++ b/app/assets/javascripts/LabelManager.js
@@ -3,6 +3,7 @@
LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
function LabelManager(opts) {
+ // Defaults
var ref, ref1, ref2;
if (opts == null) {
opts = {};
@@ -28,6 +29,7 @@
$btn = $(e.currentTarget);
$label = $("#" + ($btn.data('domId')));
action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
+ // Make sure tooltip will hide
$tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
$tooltip.tooltip('destroy');
return _this.toggleLabelPriority($label, action);
@@ -42,6 +44,7 @@
url = $label.find('.js-toggle-priority').data('url');
$target = this.prioritizedLabels;
$from = this.otherLabels;
+ // Optimistic update
if (action === 'remove') {
$target = this.otherLabels;
$from = this.prioritizedLabels;
@@ -53,6 +56,7 @@
$target.find('.empty-message').addClass('hidden');
}
$label.detach().appendTo($target);
+ // Return if we are not persisting state
if (!persistState) {
return;
}
@@ -61,6 +65,7 @@
url: url,
type: 'DELETE'
});
+ // Restore empty message
if (!$from.find('li').length) {
$from.find('.empty-message').removeClass('hidden');
}
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 84b292e59c6..6df2ecf57a2 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -24,6 +24,8 @@
return callback(group);
});
},
+ // Return groups list. Filtered by query
+ // Only active groups retrieved
groups: function(query, skip_ldap, callback) {
var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
@@ -38,6 +40,7 @@
return callback(groups);
});
},
+ // Return namespaces list. Filtered by query
namespaces: function(query, callback) {
var url = Api.buildUrl(Api.namespacesPath);
return $.ajax({
@@ -52,6 +55,7 @@
return callback(namespaces);
});
},
+ // Return projects list. Filtered by query
projects: function(query, order, callback) {
var url = Api.buildUrl(Api.projectsPath);
return $.ajax({
@@ -82,6 +86,7 @@
return callback(message.responseJSON);
});
},
+ // Return group projects list. Filtered by query
groupProjects: function(group_id, query, callback) {
var url = Api.buildUrl(Api.groupProjectsPath)
.replace(':id', group_id);
@@ -97,6 +102,7 @@
return callback(projects);
});
},
+ // Return text for a specific license
licenseText: function(key, data, callback) {
var url = Api.buildUrl(Api.licensePath)
.replace(':key', key);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 43a679501a7..31fa508d6c1 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,3 +1,9 @@
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
/*= require jquery2 */
/*= require jquery-ui/autocomplete */
/*= require jquery-ui/datepicker */
@@ -76,6 +82,7 @@
}
};
+ // Disable button if text field is empty
window.disableButtonIfEmptyField = function(field_selector, button_selector) {
var closest_submit, field;
field = $(field_selector);
@@ -92,6 +99,7 @@
});
};
+ // Disable button if any input field with given selector is empty
window.disableButtonIfAnyEmptyField = function(form, form_selector, button_selector) {
var closest_submit, updateButtons;
closest_submit = form.find(button_selector);
@@ -128,6 +136,8 @@
window.addEventListener("hashchange", shiftWindow);
window.onload = function() {
+ // Scroll the window to avoid the topnav bar
+ // https://github.com/twitter/bootstrap/issues/1768
if (location.hash) {
return setTimeout(shiftWindow, 100);
}
@@ -149,6 +159,8 @@
return $(this).select().one('mouseup', function(e) {
return e.preventDefault();
});
+ // Click a .js-select-on-focus field, select the contents
+ // Prevent a mouseup event from deselecting the input
});
$('.remove-row').bind('ajax:success', function() {
$(this).tooltip('destroy')
@@ -163,6 +175,7 @@
});
$('select.select2').select2({
width: 'resolve',
+ // Initialize select2 selects
dropdownAutoWidth: true
});
$('.js-select2').bind('select2-close', function() {
@@ -170,25 +183,28 @@
$('.select2-container-active').removeClass('select2-container-active');
return $(':focus').blur();
}), 1);
+ // Close select2 on escape
});
+ // Initialize tooltips
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
placement: function(_, el) {
- var $el;
- $el = $(el);
- return $el.data('placement') || 'bottom';
+ return $(el).data('placement') || 'bottom';
}
});
$('.trigger-submit').on('change', function() {
return $(this).parents('form').submit();
+ // Form submitter
});
gl.utils.localTimeAgo($('abbr.timeago, .js-timeago'), true);
+ // Flash
if ((flash = $(".flash-container")).length > 0) {
flash.click(function() {
return $(this).fadeOut();
});
flash.show();
}
+ // Disable form buttons while a form is submitting
$body.on('ajax:complete, ajax:beforeSend, submit', 'form', function(e) {
var buttons;
buttons = $('[type="submit"]', this);
@@ -209,6 +225,7 @@
}
});
$('.account-box').hover(function() {
+ // Show/Hide the profile menu when hovering the account box
return $(this).toggleClass('hover');
});
$document.on('click', '.diff-content .js-show-suppressed-diff', function() {
@@ -216,6 +233,7 @@
$container = $(this).parent();
$container.next('table').show();
return $container.remove();
+ // Commit show suppressed diff
});
$('.navbar-toggle').on('click', function() {
$('.header-content .title').toggle();
@@ -223,6 +241,7 @@
$('.header-content .navbar-collapse').toggle();
return $('.navbar-toggle').toggleClass('active');
});
+ // Show/hide comments on diff
$body.on("click", ".js-toggle-diff-comments", function(e) {
var $this = $(this);
$this.toggleClass('active');
@@ -286,42 +305,9 @@
gl.awardsHandler = new AwardsHandler();
checkInitialSidebarSize();
new Aside();
- if ($window.width() < 1024 && $.cookie('pin_nav') === 'true') {
- $.cookie('pin_nav', 'false', {
- path: gon.relative_url_root || '/',
- expires: 365 * 10
- });
- $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded').removeClass('page-sidebar-pinned');
- $('.navbar-fixed-top').removeClass('header-pinned-nav');
- }
- $document.off('click', '.js-nav-pin').on('click', '.js-nav-pin', function(e) {
- var $page, $pinBtn, $tooltip, $topNav, doPinNav, tooltipText;
- e.preventDefault();
- $pinBtn = $(e.currentTarget);
- $page = $('.page-with-sidebar');
- $topNav = $('.navbar-fixed-top');
- $tooltip = $("#" + ($pinBtn.attr('aria-describedby')));
- doPinNav = !$page.is('.page-sidebar-pinned');
- tooltipText = 'Pin navigation';
- $(this).toggleClass('is-active');
- if (doPinNav) {
- $page.addClass('page-sidebar-pinned');
- $topNav.addClass('header-pinned-nav');
- } else {
- $tooltip.remove();
- $page.removeClass('page-sidebar-pinned').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
- $topNav.removeClass('header-pinned-nav').toggleClass('header-collapsed header-expanded');
- }
- $.cookie('pin_nav', doPinNav, {
- path: gon.relative_url_root || '/',
- expires: 365 * 10
- });
- if ($.cookie('pin_nav') === 'true' || doPinNav) {
- tooltipText = 'Unpin navigation';
- }
- $tooltip.find('.tooltip-inner').text(tooltipText);
- return $pinBtn.attr('title', tooltipText).tooltip('fixTitle');
- });
+
+ // bind sidebar events
+ new gl.Sidebar();
// Custom time ago
gl.utils.shortTimeAgo($('.js-short-timeago'));
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 7116512d6b7..a9aec6e8ea4 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -16,7 +16,7 @@
}
Autosave.prototype.restore = function() {
- var e, error, text;
+ var e, text;
if (window.localStorage == null) {
return;
}
@@ -41,7 +41,7 @@
if ((text != null ? text.length : void 0) > 0) {
try {
return window.localStorage.setItem(this.key, text);
- } catch (undefined) {}
+ } catch (error) {}
} else {
return this.reset();
}
@@ -53,7 +53,7 @@
}
try {
return window.localStorage.removeItem(this.key);
- } catch (undefined) {}
+ } catch (error) {}
};
return Autosave;
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index ad12cb906e1..0decc6d09e6 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -86,6 +86,8 @@
AwardsHandler.prototype.positionMenu = function($menu, $addBtn) {
var css, position;
position = $addBtn.data('position');
+ // The menu could potentially be off-screen or in a hidden overflow element
+ // So we position the element absolute in the body
css = {
top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px"
};
@@ -255,12 +257,12 @@
};
AwardsHandler.prototype.animateEmoji = function($emoji) {
- var className;
- className = 'pulse animated';
+ var className = 'pulse animated once short';
$emoji.addClass(className);
- return setTimeout((function() {
- return $emoji.removeClass(className);
- }), 321);
+
+ $emoji.on('webkitAnimationEnd animationEnd', function() {
+ $(this).removeClass(className);
+ });
};
AwardsHandler.prototype.createEmoji = function(votesBlock, emoji) {
@@ -284,6 +286,7 @@
if (emojiIcon.length > 0) {
unicodeName = emojiIcon.data('unicode-name');
} else {
+ // Find by alias
unicodeName = $(".emoji-menu-content [data-aliases*=':" + emoji + ":']").data('unicode-name');
}
return "emoji-" + unicodeName;
@@ -350,8 +353,10 @@
return function(ev) {
var found_emojis, h5, term, ul;
term = $(ev.target).val();
+ // Clean previous search results
$('ul.emoji-menu-search, h5.emoji-search').remove();
if (term) {
+ // Generate a search result block
h5 = $('<h5>').text('Search results');
found_emojis = _this.searchEmojis(term).show();
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index f977a1e8a7b..dc8ae601961 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,7 +1,5 @@
/*= require jquery.ba-resize */
-
-
/*= require autosize */
(function() {
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index 3631d1b74ac..1df681a4816 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -5,6 +5,12 @@
container = $(this).closest(".js-details-container");
return container.toggleClass("open");
});
+ // Show details content. Hides link after click.
+ //
+ // %div
+ // %a.js-details-expand
+ // %div.js-details-content
+ //
return $("body").on("click", ".js-details-expand", function(e) {
$(this).next('.js-details-content').removeClass("hide");
$(this).hide();
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 3527d0a95fc..54b7360ab41 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,6 +1,20 @@
-
+// Quick Submit behavior
+//
+// When a child field of a form with a `js-quick-submit` class receives a
+// "Meta+Enter" (Mac) or "Ctrl+Enter" (Linux/Windows) key combination, the form
+// is submitted.
+//
/*= require extensions/jquery */
+//
+// ### Example Markup
+//
+// <form action="/foo" class="js-quick-submit">
+// <input type="text" />
+// <textarea></textarea>
+// <input type="submit" value="Submit" />
+// </form>
+//
(function() {
var isMac, keyCodeIs;
@@ -17,6 +31,7 @@
$(document).on('keydown.quick_submit', '.js-quick-submit', function(e) {
var $form, $submit_button;
+ // Enter
if (!keyCodeIs(e, 13)) {
return;
}
@@ -33,8 +48,11 @@
return $form.submit();
});
+ // If the user tabs to a submit button on a `js-quick-submit` form, display a
+ // tooltip to let them know they could've used the hotkey
$(document).on('keyup.quick_submit', '.js-quick-submit input[type=submit], .js-quick-submit button[type=submit]', function(e) {
var $this, title;
+ // Tab
if (!keyCodeIs(e, 9)) {
return;
}
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index db0b36b24e9..894034bdd54 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,6 +1,18 @@
-
+// Requires Input behavior
+//
+// When called on a form with input fields with the `required` attribute, the
+// form's submit button will be disabled until all required fields have values.
+//
/*= require extensions/jquery */
+//
+// ### Example Markup
+//
+// <form class="js-requires-input">
+// <input type="text" required="required">
+// <input type="submit" value="Submit">
+// </form>
+//
(function() {
$.fn.requiresInput = function() {
var $button, $form, fieldSelector, requireInput, required;
@@ -11,14 +23,17 @@
requireInput = function() {
var values;
values = _.map($(fieldSelector, $form), function(field) {
+ // Collect the input values of *all* required fields
return field.value;
});
+ // Disable the button if any required fields are empty
if (values.length && _.any(values, _.isEmpty)) {
return $button.disable();
} else {
return $button.enable();
}
};
+ // Set initial button state
requireInput();
return $form.on('change input', fieldSelector, requireInput);
};
@@ -27,6 +42,8 @@
var $form, hideOrShowHelpBlock;
$form = $('form.js-requires-input');
$form.requiresInput();
+ // Hide or Show the help block when creating a new project
+ // based on the option selected
hideOrShowHelpBlock = function(form) {
var selected;
selected = $('.js-select-namespace option:selected');
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 5467e3edc69..a6ce378d67a 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,5 +1,12 @@
(function(w) {
$(function() {
+ // Toggle button. Show/hide content inside parent container.
+ // Button does not change visibility. If button has icon - it changes chevron style.
+ //
+ // %div.js-toggle-container
+ // %a.js-toggle-button
+ // %div.js-toggle-content
+ //
$('body').on('click', '.js-toggle-button', function(e) {
e.preventDefault();
$(this)
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index f4044f22db2..8cca1aa9232 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -8,6 +8,8 @@
autoDiscover: false,
autoProcessQueue: false,
url: form.attr('action'),
+ // Rails uses a hidden input field for PUT
+ // http://stackoverflow.com/questions/21056482/how-to-set-method-put-in-form-tag-in-rails
method: method,
clickable: true,
uploadMultiple: false,
@@ -36,6 +38,7 @@
formData.append('commit_message', form.find('.js-commit-message').val());
});
},
+ // Override behavior of adding error underneath preview
error: function(file, errorMessage) {
var stripped;
stripped = $("<div/>").html(errorMessage).text();
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
index b0a37ef0e0a..b18b6962382 100644
--- a/app/assets/javascripts/blob/template_selector.js
+++ b/app/assets/javascripts/blob/template_selector.js
@@ -66,6 +66,9 @@
// be added by all subclasses.
};
+ // To be implemented on the extending class
+ // e.g.
+ // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
this.editor.setValue(file.content, 1);
if (!skipFocus) this.editor.focus();
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 649c79daee8..b846bab0424 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -18,6 +18,8 @@
return function() {
return $("#file-content").val(_this.editor.getValue());
};
+ // Before a form submission, move the content from the Ace editor into the
+ // submitted textarea
})(this));
this.initModePanesAndLinks();
new BlobLicenseSelectors({
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index f9f9f7999d4..b5ff3a81ed5 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,10 +1,7 @@
-Vue.http.interceptors.push((request, next) => {
+Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
- Vue.nextTick(() => {
- setTimeout(() => {
- Vue.activeResources--;
- }, 500);
+ next(function (response) {
+ Vue.activeResources--;
});
- next();
});
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 1e0148e5798..5fef9725178 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -23,6 +23,7 @@
if ($(allDeviceSelector.join(",")).length) {
return;
}
+ // Create all the elements
els = $.map(BREAKPOINTS, function(breakpoint) {
return "<div class='device-" + breakpoint + " visible-" + breakpoint + "'></div>";
});
@@ -40,6 +41,7 @@
BreakpointInstance.prototype.getBreakpointSize = function() {
var $visibleDevice;
$visibleDevice = this.visibleDevice;
+ // the page refreshed via turbolinks
if (!$visibleDevice().length) {
this.setup();
}
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 0d7d29bb0d0..10abeb50f4b 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -16,6 +16,7 @@
this.toggleSidebar = bind(this.toggleSidebar, this);
this.updateDropdown = bind(this.updateDropdown, this);
clearInterval(Build.interval);
+ // Init breakpoint checker
this.bp = Breakpoints.get();
$('.js-build-sidebar').niceScroll();
@@ -42,6 +43,9 @@
$(this).data("state", "enabled");
return $(this).text("disable autoscroll");
}
+ //
+ // Bind autoscroll button to follow build output
+ //
});
Build.interval = setInterval((function(_this) {
return function() {
@@ -49,17 +53,23 @@
return _this.getBuildTrace();
}
};
+ //
+ // Check for new build output if user still watching build page
+ // Only valid for runnig build when output changes during time
+ //
})(this), 4000);
}
}
Build.prototype.getInitialBuildTrace = function() {
+ var removeRefreshStatuses = ['success', 'failed', 'canceled', 'skipped']
+
return $.ajax({
url: this.build_url,
dataType: 'json',
success: function(build_data) {
$('.js-build-output').html(build_data.trace_html);
- if (build_data.status === 'success' || build_data.status === 'failed') {
+ if (removeRefreshStatuses.indexOf(build_data.status) >= 0) {
return $('.js-build-refresh').remove();
}
}
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
new file mode 100644
index 00000000000..8d3e29794a1
--- /dev/null
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -0,0 +1,6 @@
+$(function(){
+ $('.reveal-variables').off('click').on('click',function(){
+ $('.js-build').toggle().niceScroll();
+ $(this).hide();
+ });
+});
diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image-file.js
index c0d0b2d049f..e893491b19b 100644
--- a/app/assets/javascripts/commit/image-file.js
+++ b/app/assets/javascripts/commit/image-file.js
@@ -2,6 +2,7 @@
this.ImageFile = (function() {
var prepareFrames;
+ // Width where images must fits in, for 2-up this gets divided by 2
ImageFile.availWidth = 900;
ImageFile.viewModes = ['two-up', 'swipe'];
@@ -9,6 +10,7 @@
function ImageFile(file) {
this.file = file;
this.requestImageInfo($('.two-up.view .frame.deleted img', this.file), (function(_this) {
+ // Determine if old and new file has same dimensions, if not show 'two-up' view
return function(deletedWidth, deletedHeight) {
return _this.requestImageInfo($('.two-up.view .frame.added img', _this.file), function(width, height) {
if (width === deletedWidth && height === deletedHeight) {
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 37f168c5190..9132089adcd 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -45,6 +45,7 @@
CommitsList.content.html(data.html);
return history.replaceState({
page: commitsUrl
+ // Change url so if user reload a page - search results are saved
}, document.title, commitsUrl);
},
dataType: "json"
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index c43af17442b..3e20db7e308 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -6,14 +6,19 @@
genericSuccess = function(e) {
showTooltip(e.trigger, 'Copied!');
+ // Clear the selection and blur the trigger so it loses its border
e.clearSelection();
return $(e.trigger).blur();
};
+ // Safari doesn't support `execCommand`, so instead we inform the user to
+ // copy manually.
+ //
+ // See http://clipboardjs.com/#browser-support
genericError = function(e) {
var key;
if (/Mac/i.test(navigator.userAgent)) {
- key = '&#8984;';
+ key = '&#8984;'; // Command
} else {
key = 'Ctrl';
}
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 3dd7ceba92f..c8634b78f2b 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -39,6 +39,9 @@
bottom: unfoldBottom,
offset: offset,
unfold: unfold,
+ // indent is used to compensate for single space indent to fit
+ // '+' and '-' prepended to diff lines,
+ // see https://gitlab.com/gitlab-org/gitlab-ce/issues/707
indent: 1,
view: file.data('view')
};
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 38cdc7b9fba..99b16f7d59b 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -23,6 +23,7 @@
case 'projects:boards:show':
shortcut_handler = new ShortcutsNavigation();
break;
+ case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
new IssuableBulkActions();
@@ -93,10 +94,6 @@
break;
case "projects:merge_requests:conflicts":
window.mcui = new MergeConflictResolver()
- case 'projects:merge_requests:index':
- shortcut_handler = new ShortcutsNavigation();
- Issuable.init();
- break;
case 'dashboard:activity':
new Activities();
break;
@@ -167,6 +164,8 @@
}
break;
case 'projects:network:show':
+ // Ensure we don't create a particular shortcut handler here. This is
+ // already created, where the network graph is created.
shortcut_handler = true;
break;
case 'projects:forks:new':
@@ -263,12 +262,14 @@
shortcut_handler = new ShortcutsNavigation();
}
}
+ // If we haven't installed a custom shortcut handler, install the default one
if (!shortcut_handler) {
return new Shortcuts();
}
};
Dispatcher.prototype.initSearch = function() {
+ // Only when search form is present
if ($('.search').length) {
return new SearchAutocomplete();
}
diff --git a/app/assets/javascripts/due_date_select.js b/app/assets/javascripts/due_date_select.js
index 5a725a41fd1..bf68b7e3a9b 100644
--- a/app/assets/javascripts/due_date_select.js
+++ b/app/assets/javascripts/due_date_select.js
@@ -2,6 +2,7 @@
this.DueDateSelect = (function() {
function DueDateSelect() {
var $datePicker, $dueDate, $loading;
+ // Milestone edit/new form
$datePicker = $('.datepicker');
if ($datePicker.length) {
$dueDate = $('#milestone_due_date');
@@ -16,6 +17,7 @@
e.preventDefault();
return $.datepicker._clearDate($datePicker);
});
+ // Issuable sidebar
$loading = $('.js-issuable-update .due_date').find('.block-loading').hide();
$('.js-due-date-select').each(function(i, dropdown) {
var $block, $dropdown, $dropdownParent, $selectbox, $sidebarValue, $value, $valueContent, abilityName, addDueDate, fieldName, issueUpdateURL;
@@ -38,6 +40,7 @@
});
addDueDate = function(isDropdown) {
var data, date, mediumDate, value;
+ // Create the post date
value = $("input[name='" + fieldName + "']").val();
if (value !== '') {
date = new Date(value.replace(new RegExp('-', 'g'), ','));
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index ae3dde63da3..4978e24949c 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,3 +1,4 @@
+// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
disable: function() {
@@ -5,6 +6,7 @@
}
});
+ // Enable an element and remove the 'disabled' Bootstrap class
$.fn.extend({
enable: function() {
return $(this).removeAttr('disabled').removeClass('disabled');
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 3dca06d36b1..d0786bf0053 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,3 +1,4 @@
+// Creates the variables for setting up GFM auto-completion
(function() {
if (window.GitLab == null) {
window.GitLab = {};
@@ -8,18 +9,22 @@
dataLoaded: false,
cachedData: {},
dataSource: '',
+ // Emoji
Emoji: {
template: '<li>${name} <img alt="${name}" height="20" src="${path}" width="20" /></li>'
},
+ // Team Members
Members: {
template: '<li>${username} <small>${title}</small></li>'
},
Labels: {
template: '<li><span class="dropdown-label-box" style="background: ${color}"></span> ${title}</li>'
},
+ // Issues and MergeRequests
Issues: {
template: '<li><small>${id}</small> ${title}</li>'
},
+ // Milestones
Milestones: {
template: '<li>${title}</li>'
},
@@ -48,8 +53,11 @@
}
},
setup: function(input) {
+ // Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
+ // destroy previous instances
this.destroyAtWho();
+ // set up instances
this.setupAtWho();
if (this.dataSource) {
if (!this.dataLoading && !this.cachedData) {
@@ -63,6 +71,11 @@
return _this.loadData(data);
});
};
+ // We should wait until initializations are done
+ // and only trigger the last .setup since
+ // The previous .dataSource belongs to the previous issuable
+ // and the last one will have the **proper** .dataSource property
+ // TODO: Make this a singleton and turn off events when moving to another page
})(this), 1000);
}
if (this.cachedData != null) {
@@ -71,6 +84,7 @@
}
},
setupAtWho: function() {
+ // Emoji
this.input.atwho({
at: ':',
displayTpl: (function(_this) {
@@ -90,6 +104,7 @@
beforeInsert: this.DefaultOptions.beforeInsert
}
});
+ // Team Members
this.input.atwho({
at: '@',
displayTpl: (function(_this) {
@@ -321,13 +336,22 @@
loadData: function(data) {
this.cachedData = data;
this.dataLoaded = true;
+ // load members
this.input.atwho('load', '@', data.members);
+ // load issues
this.input.atwho('load', 'issues', data.issues);
+ // load milestones
this.input.atwho('load', 'milestones', data.milestones);
+ // load merge requests
this.input.atwho('load', 'mergerequests', data.mergerequests);
+ // load emojis
this.input.atwho('load', ':', data.emojis);
+ // load labels
this.input.atwho('load', '~', data.labels);
+ // load commands
this.input.atwho('load', '/', data.commands);
+ // This trigger at.js again
+ // otherwise we would be stuck with loading until the user types
return $(':focus').trigger('keyup');
}
};
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 77b2082cba0..bea141bae51 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -21,12 +21,14 @@
$clearButton = $inputContainer.find('.js-dropdown-input-clear');
this.indeterminateIds = [];
$clearButton.on('click', (function(_this) {
+ // Clear click
return function(e) {
e.preventDefault();
e.stopPropagation();
return _this.input.val('').trigger('keyup').focus();
};
})(this));
+ // Key events
timeout = "";
this.input
.on('keydown', function (e) {
@@ -49,6 +51,7 @@
if (keyCode === 13 && !options.elIsInput) {
return false;
}
+ // Only filter asynchronously only if option remote is set
if (this.options.remote) {
clearTimeout(timeout);
return timeout = setTimeout(function() {
@@ -79,11 +82,27 @@
if ((data != null) && !this.options.filterByText) {
results = data;
if (search_text !== '') {
+ // When data is an array of objects therefore [object Array] e.g.
+ // [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ]
if (_.isArray(data)) {
results = fuzzaldrinPlus.filter(data, search_text, {
key: this.options.keys
});
} else {
+ // If data is grouped therefore an [object Object]. e.g.
+ // {
+ // groupName1: [
+ // { prop: 'foo' },
+ // { prop: 'baz' }
+ // ],
+ // groupName2: [
+ // { prop: 'abc' },
+ // { prop: 'def' }
+ // ]
+ // }
if (gl.utils.isObject(data)) {
results = {};
for (key in data) {
@@ -140,6 +159,7 @@
this.options.beforeSend();
}
return this.dataEndpoint("", (function(_this) {
+ // Fetch the data by calling the data funcfion
return function(data) {
if (_this.options.success) {
_this.options.success(data);
@@ -171,6 +191,7 @@
};
})(this)
});
+ // Fetch the data through ajax if the data is a string
};
return GitLabDropdownRemote;
@@ -209,13 +230,18 @@
self = this;
selector = $(this.el).data("target");
this.dropdown = selector != null ? $(selector) : $(this.el).parent();
+ // Set Defaults
ref = this.options, this.filterInput = (ref1 = ref.filterInput) != null ? ref1 : this.getElement(FILTER_INPUT), this.highlight = (ref2 = ref.highlight) != null ? ref2 : false, this.filterInputBlur = (ref3 = ref.filterInputBlur) != null ? ref3 : true;
+ // If no input is passed create a default one
self = this;
+ // If selector was passed
if (_.isString(this.filterInput)) {
this.filterInput = this.getElement(this.filterInput);
}
searchFields = this.options.search ? this.options.search.fields : [];
if (this.options.data) {
+ // If we provided data
+ // data could be an array of objects or a group of arrays
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.fullData = this.options.data;
currentIndex = -1;
@@ -232,10 +258,12 @@
return _this.filter.input.trigger('keyup');
}
};
+ // Remote data
})(this)
});
}
}
+ // Init filterable
if (this.options.filterable) {
this.filter = new GitLabDropdownFilter(this.filterInput, {
elIsInput: $(this.el).is('input'),
@@ -278,12 +306,14 @@
})(this)
});
}
+ // Event listeners
this.dropdown.on("shown.bs.dropdown", this.opened);
this.dropdown.on("hidden.bs.dropdown", this.hidden);
$(this.el).on("update.label", this.updateLabel);
this.dropdown.on("click", ".dropdown-menu, .dropdown-menu-close", this.shouldPropagate);
this.dropdown.on('keyup', (function(_this) {
return function(e) {
+ // Escape key
if (e.which === 27) {
return $('.dropdown-menu-close', _this.dropdown).trigger('click');
}
@@ -327,6 +357,7 @@
}
}
+ // Finds an element inside wrapper element
GitLabDropdown.prototype.getElement = function(selector) {
return this.dropdown.find(selector);
};
@@ -344,6 +375,7 @@
}
}
menu.toggleClass(PAGE_TWO_CLASS);
+ // Focus first visible input on active page
return this.dropdown.find('[class^="dropdown-page-"]:visible :text:visible:first').focus();
};
@@ -351,23 +383,28 @@
var full_html, groupData, html, name;
this.renderedData = data;
if (this.options.filterable && data.length === 0) {
+ // render no matching results
html = [this.noResults()];
} else {
+ // Handle array groups
if (gl.utils.isObject(data)) {
html = [];
for (name in data) {
groupData = data[name];
html.push(this.renderItem({
header: name
+ // Add header for each group
}, name));
this.renderData(groupData, name).map(function(item) {
return html.push(item);
});
}
} else {
+ // Render each row
html = this.renderData(data);
}
}
+ // Render the full menu
full_html = this.renderMenu(html);
return this.appendMenu(full_html);
};
@@ -406,6 +443,7 @@
if (this.options.setActiveIds) {
this.options.setActiveIds.call(this);
}
+ // Makes indeterminate items effective
if (this.fullData && this.dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')) {
this.parseData(this.fullData);
}
@@ -427,6 +465,8 @@
if (this.options.filterable) {
$input.blur().val("");
}
+ // Triggering 'keyup' will re-render the dropdown which is not always required
+ // specially if we want to keep the state of the dropdown needed for bulk-assignment
if (!this.options.persistWhenHide) {
$input.trigger("keyup");
}
@@ -439,6 +479,7 @@
return this.dropdown.trigger('hidden.gl.dropdown');
};
+ // Render the full menu
GitLabDropdown.prototype.renderMenu = function(html) {
var menu_html;
menu_html = "";
@@ -450,6 +491,7 @@
return menu_html;
};
+ // Append the menu into the dropdown
GitLabDropdown.prototype.appendMenu = function(html) {
var selector;
selector = '.dropdown-content';
@@ -465,19 +507,24 @@
group = false;
}
if (index == null) {
+ // Render the row
index = false;
}
html = "";
+ // Divider
if (data === "divider") {
return "<li class='divider'></li>";
}
+ // Separator is a full-width divider
if (data === "separator") {
return "<li class='separator'></li>";
}
+ // Header
if (data.header != null) {
return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
}
if (this.options.renderRow) {
+ // Call the render function
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
@@ -489,11 +536,13 @@
selected = true;
}
}
+ // Set URL
if (this.options.url != null) {
url = this.options.url(data);
} else {
url = data.url != null ? data.url : '#';
}
+ // Set Text
if (this.options.text != null) {
text = this.options.text(data);
} else {
@@ -584,6 +633,7 @@
if (value == null) {
field.remove();
}
+ // Toggle active class for the tick mark
el.addClass(ACTIVE_CLASS);
if (value != null) {
if (!field.length && fieldName) {
@@ -604,6 +654,7 @@
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
+ // Create hidden input for form
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
@@ -625,6 +676,7 @@
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
+ // simulate a click on the first link
$el = $(selector, this.dropdown);
if ($el.length) {
var href = $el.attr('href');
@@ -653,11 +705,15 @@
e.stopImmediatePropagation();
PREV_INDEX = currentIndex;
$listItems = $(selector, _this.dropdown);
+ // if @options.filterable
+ // $input.blur()
if (currentKeyCode === 40) {
+ // Move down
if (currentIndex < ($listItems.length - 1)) {
currentIndex += 1;
}
} else if (currentKeyCode === 38) {
+ // Move up
if (currentIndex > 0) {
currentIndex -= 1;
}
@@ -685,24 +741,32 @@
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
+ // Remove the class for the previously focused row
$('.is-focused', this.dropdown).removeClass('is-focused');
+ // Update the class for the row at the specific index
$listItem = $listItems.eq(index);
$listItem.find('a:first-child').addClass("is-focused");
+ // Dropdown content scroll area
$dropdownContent = $listItem.closest('.dropdown-content');
dropdownScrollTop = $dropdownContent.scrollTop();
dropdownContentHeight = $dropdownContent.outerHeight();
dropdownContentTop = $dropdownContent.prop('offsetTop');
dropdownContentBottom = dropdownContentTop + dropdownContentHeight;
+ // Get the offset bottom of the list item
listItemHeight = $listItem.outerHeight();
listItemTop = $listItem.prop('offsetTop');
listItemBottom = listItemTop + listItemHeight;
if (!index) {
+ // Scroll the dropdown content to the top
$dropdownContent.scrollTop(0)
} else if (index === ($listItems.length - 1)) {
+ // Scroll the dropdown content to the bottom
$dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
} else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ // Scroll the dropdown content down
$dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
} else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ // Scroll the dropdown content up
return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
}
};
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 528a673eb15..2703adc0705 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -3,12 +3,15 @@
function GLForm(form) {
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
+ // Before we start, we should clean up any previous data for this form
this.destroy();
+ // Setup the form
this.setupForm();
this.form.data('gl-form', this);
}
GLForm.prototype.destroy = function() {
+ // Clean form listeners
this.clearEventListeners();
return this.form.data('gl-form', null);
};
@@ -21,12 +24,15 @@
this.form.find('.div-dropzone').remove();
this.form.addClass('gfm-form');
disableButtonIfEmptyField(this.form.find('.js-note-text'), this.form.find('.js-comment-button'));
+ // remove notify commit author checkbox for non-commit notes
GitLab.GfmAutoComplete.setup(this.form.find('.js-gfm-input'));
new DropzoneInput(this.form);
autosize(this.textarea);
+ // form and textarea event listeners
this.addEventListeners();
gl.text.init(this.form);
}
+ // hide discard button
this.form.find('.js-note-discard').hide();
return this.form.show();
};
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index b95faadc8e7..4886da9f21f 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,7 +1,11 @@
-
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
/*= require_tree . */
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index a646ca1d84f..7d9d4d7c679 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -204,6 +204,7 @@
function ContributorsAuthorGraph(data1) {
this.data = data1;
+ // Don't split graph size in half for mobile devices.
if ($(window).width() < 768) {
this.width = $('.content').width() - 80;
} else {
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index fd5b6dc0ddd..7c2eebcdd44 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -38,6 +38,7 @@
return _this.formatSelection.apply(_this, args);
},
dropdownCssClass: "ajax-groups-dropdown",
+ // we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 0f840821f53..9efad1ce943 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -10,21 +10,24 @@
ImporterStatus.prototype.initStatusPage = function() {
$('.js-add-to-import').off('click').on('click', (function(_this) {
return function(e) {
- var $btn, $namespace_input, $target_field, $tr, id, new_namespace;
+ var $btn, $namespace_input, $target_field, $tr, id, target_namespace;
$btn = $(e.currentTarget);
$tr = $btn.closest('tr');
$target_field = $tr.find('.import-target');
$namespace_input = $target_field.find('input');
id = $tr.attr('id').replace('repo_', '');
- new_namespace = null;
+ target_namespace = null;
+
if ($namespace_input.length > 0) {
- new_namespace = $namespace_input.prop('value');
- $target_field.empty().append(new_namespace + "/" + ($target_field.data('project_name')));
+ target_namespace = $namespace_input.prop('value');
+ $target_field.empty().append(target_namespace + "/" + ($target_field.data('project_name')));
}
+
$btn.disable().addClass('is-loading');
+
return $.post(_this.import_url, {
repo_id: id,
- new_namespace: new_namespace
+ target_namespace: target_namespace
}, {
dataType: 'script'
});
@@ -70,7 +73,7 @@
if ($('.js-importer-status').length) {
var jobsImportPath = $('.js-importer-status').data('jobs-import-path');
var importPath = $('.js-importer-status').data('import-path');
-
+
new ImporterStatus(jobsImportPath, importPath);
}
});
diff --git a/app/assets/javascripts/issuable.js b/app/assets/javascripts/issuable.js.es6
index d0305c6c6a1..53faaa38a0c 100644
--- a/app/assets/javascripts/issuable.js
+++ b/app/assets/javascripts/issuable.js.es6
@@ -8,6 +8,7 @@
Issuable.initTemplates();
Issuable.initSearch();
Issuable.initChecks();
+ Issuable.initResetFilters();
return Issuable.initLabelFilterRemove();
},
initTemplates: function() {
@@ -37,9 +38,11 @@
return $(document).off('click', '.js-label-filter-remove').on('click', '.js-label-filter-remove', function(e) {
var $button;
$button = $(this);
+ // Remove the label input box
$('input[name="label_name[]"]').filter(function() {
return this.value === $button.data('label');
}).remove();
+ // Submit the form to get new data
Issuable.filterResults($('.filter-form'));
return $('.js-label-select').trigger('update.label');
});
@@ -55,6 +58,17 @@
return Turbolinks.visit(issuesUrl);
};
})(this),
+ initResetFilters: function() {
+ $('.reset-filters').on('click', function(e) {
+ e.preventDefault();
+ const target = e.target;
+ const $form = $(target).parents('.js-filter-form');
+ const baseIssuesUrl = target.href;
+
+ $form.attr('action', baseIssuesUrl);
+ Turbolinks.visit(baseIssuesUrl);
+ });
+ },
initChecks: function() {
this.issuableBulkActions = $('.bulk-update').data('bulkActions');
$('.check_all_issues').off('click').on('click', function() {
@@ -64,19 +78,22 @@
return $('.selected_issue').off('change').on('change', Issuable.checkChanged.bind(this));
},
checkChanged: function() {
- var checked_issues, ids;
- checked_issues = $('.selected_issue:checked');
- if (checked_issues.length > 0) {
- ids = $.map(checked_issues, function(value) {
+ const $checkedIssues = $('.selected_issue:checked');
+ const $updateIssuesIds = $('#update_issuable_ids');
+ const $issuesOtherFilters = $('.issues-other-filters');
+ const $issuesBulkUpdate = $('.issues_bulk_update');
+
+ if ($checkedIssues.length > 0) {
+ let ids = $.map($checkedIssues, function(value) {
return $(value).data('id');
});
- $('#update_issues_ids').val(ids);
- $('.issues-other-filters').hide();
- $('.issues_bulk_update').show();
+ $updateIssuesIds.val(ids);
+ $issuesOtherFilters.hide();
+ $issuesBulkUpdate.show();
} else {
- $('#update_issues_ids').val([]);
- $('.issues_bulk_update').hide();
- $('.issues-other-filters').show();
+ $updateIssuesIds.val([]);
+ $issuesBulkUpdate.hide();
+ $issuesOtherFilters.show();
this.issuableBulkActions.willUpdateLabels = false;
}
return true;
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index e6422602ce8..261bf6137c2 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,10 +1,6 @@
/*= require flash */
-
-
/*= require jquery.waitforimages */
-
-
/*= require task_list */
(function() {
@@ -13,6 +9,7 @@
this.Issue = (function() {
function Issue() {
this.submitNoteForm = bind(this.submitNoteForm, this);
+ // Prevent duplicate event bindings
this.disableTaskList();
if ($('a.btn-close').length) {
this.initTaskList();
@@ -99,6 +96,8 @@
url: $('form.js-issuable-update').attr('action'),
data: patchData
});
+ // TODO (rspeicher): Make the issue description inline-editable like a note so
+ // that we can re-use its form here
};
Issue.prototype.initMergeRequests = function() {
@@ -128,6 +127,8 @@
Issue.prototype.initCanCreateBranch = function() {
var $container;
$container = $('#new-branch');
+ // If the user doesn't have the required permissions the container isn't
+ // rendered at all.
if ($container.length === 0) {
return;
}
diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues-bulk-assignment.js
index 98d3358ba92..62a7fc9a06c 100644
--- a/app/assets/javascripts/issues-bulk-assignment.js
+++ b/app/assets/javascripts/issues-bulk-assignment.js
@@ -1,14 +1,17 @@
(function() {
this.IssuableBulkActions = (function() {
function IssuableBulkActions(opts) {
+ // Set defaults
var ref, ref1, ref2;
if (opts == null) {
opts = {};
}
- this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issues-list .issue');
+ this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li');
+ // Save instance
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
+ // Fixes bulk-assign not working when navigating through pages
Issuable.initChecks();
}
@@ -86,6 +89,7 @@
ref1 = this.getLabelsFromSelection();
for (j = 0, len1 = ref1.length; j < len1; j++) {
id = ref1[j];
+ // Only the ones that we are not going to keep
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
@@ -106,7 +110,7 @@
state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
milestone_id: this.form.find('input[name="update[milestone_id]"]').val(),
- issues_ids: this.form.find('input[name="update[issues_ids]"]').val(),
+ issuable_ids: this.form.find('input[name="update[issuable_ids]"]').val(),
subscription_event: this.form.find('input[name="update[subscription_event]"]').val(),
add_label_ids: [],
remove_label_ids: []
@@ -147,6 +151,8 @@
indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) {
+ // We need to exclude label IDs that will be applied
+ // By not doing this will cause issues from selection to not add labels at all
if (labelsToApply.indexOf(id) === -1) {
return result.push(id);
}
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index fe071fca67c..cb16e2ba814 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -26,13 +26,16 @@
var previewColor;
previewColor = $('input#label_color').val();
return $('div.label-color-preview').css('background-color', previewColor);
+ // Updates the the preview color with the hex-color input
};
+ // Updates the preview color with a click on a suggested color
Labels.prototype.setSuggestedColor = function(e) {
var color;
color = $(e.currentTarget).data('color');
$('input#label_color').val(color);
this.updateColorPreview();
+ // Notify the form, that color has changed
$('.label-form').trigger('keyup');
return e.preventDefault();
};
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index bab23ff5ac0..29a967a35a0 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -156,11 +156,13 @@
selectedClass.push('is-indeterminate');
}
if (active.indexOf(label.id) !== -1) {
+ // Remove is-indeterminate class if the item will be marked as active
i = selectedClass.indexOf('is-indeterminate');
if (i !== -1) {
selectedClass.splice(i, 1);
}
selectedClass.push('is-active');
+ // Add input manually
instance.addInput(this.fieldName, label.id);
}
}
@@ -172,6 +174,7 @@
}
if (label.duplicate) {
spacing = 100 / label.color.length;
+ // Reduce the colors to 4
label.color = label.color.filter(function(color, i) {
return i < 4;
});
@@ -192,11 +195,13 @@
} else {
colorEl = '';
}
+ // We need to identify which items are actually labels
if (label.id) {
selectedClass.push('label-item');
$a.attr('data-label-id', label.id);
}
$a.addClass(selectedClass.join(' ')).html(colorEl + " " + label.title);
+ // Return generated html
return $li.html($a).prop('outerHTML');
},
persistWhenHide: $dropdown.data('persistWhenHide'),
@@ -238,6 +243,7 @@
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
$selectbox.hide();
+ // display:block overrides the hide-collapse rule
$value.removeAttr('style');
if (page === 'projects:boards:show') {
return;
@@ -255,6 +261,7 @@
}
}
if ($dropdown.hasClass('js-filter-bulk-update')) {
+ // If we are persisting state we need the classes
if (!this.options.persistWhenHide) {
return $dropdown.parent().find('.is-active, .is-indeterminate').removeClass();
}
@@ -324,7 +331,9 @@
if ($('.selected_issue:checked').length) {
return;
}
+ // Remove inputs
$('.issues_bulk_update .labels-filter input[type="hidden"]').remove();
+ // Also restore button text
return $('.issues_bulk_update .labels-filter .dropdown-toggle-text').text('Label');
};
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index 8d5e52286b7..d9b07c10a49 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
index 8ee81804513..a88e640f298 100644
--- a/app/assets/javascripts/lib/cropper.js
+++ b/app/assets/javascripts/lib/cropper.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index 31e6033e756..ee1baf54803 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
index 923c575dcfe..6df427bc2b1 100644
--- a/app/assets/javascripts/lib/raphael.js
+++ b/app/assets/javascripts/lib/raphael.js
@@ -1,13 +1,8 @@
/*= require raphael */
-
-
/*= require g.raphael */
-
-
/*= require g.bar */
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index d4d5927d3b0..8fdf4646cd8 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -29,6 +29,7 @@
if (setTimeago) {
$timeagoEls.timeago();
$timeagoEls.tooltip('destroy');
+ // Recreate with custom template
return $timeagoEls.tooltip({
template: '<div class="tooltip local-timeago" role="tooltip"><div class="tooltip-arrow"></div><div class="tooltip-inner"></div></div>'
});
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb
deleted file mode 100644
index 80f9936b9c2..00000000000
--- a/app/assets/javascripts/lib/utils/emoji_aliases.js.coffee.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-gl.emojiAliases = ->
- JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>')
diff --git a/app/assets/javascripts/lib/utils/emoji_aliases.js.erb b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
new file mode 100644
index 00000000000..aeb86c9fa5b
--- /dev/null
+++ b/app/assets/javascripts/lib/utils/emoji_aliases.js.erb
@@ -0,0 +1,6 @@
+(function() {
+ gl.emojiAliases = function() {
+ return JSON.parse('<%= Gitlab::AwardEmoji.aliases.to_json %>');
+ };
+
+}).call(this);
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 42b6ac0589e..5b338b00d76 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -6,6 +6,7 @@
notification = new Notification(message, opts);
setTimeout(function() {
return notification.close();
+ // Hide the notification after X amount of seconds
}, 8000);
if (onclick) {
return notification.onclick = onclick;
@@ -22,12 +23,16 @@
body: body,
icon: icon
};
+ // Let's check if the browser supports notifications
if (!('Notification' in window)) {
+ // do nothing
} else if (Notification.permission === 'granted') {
+ // If it's okay let's create a notification
return notificationGranted(message, opts, onclick);
} else if (Notification.permission !== 'denied') {
return Notification.requestPermission(function(permission) {
+ // If the user accepts, let's create a notification
if (permission === 'granted') {
return notificationGranted(message, opts, onclick);
}
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index b6636de5767..d761a844be9 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -29,6 +29,7 @@
lineBefore = this.lineBefore(text, textArea);
lineAfter = this.lineAfter(text, textArea);
if (lineBefore === blockTag && lineAfter === blockTag) {
+ // To remove the block tag we have to select the line before & after
if (blockTag != null) {
textArea.selectionStart = textArea.selectionStart - (blockTag.length + 1);
textArea.selectionEnd = textArea.selectionEnd + (blockTag.length + 1);
@@ -63,11 +64,11 @@
if (!inserted) {
try {
document.execCommand("ms-beginUndoUnit");
- } catch (undefined) {}
+ } catch (error) {}
textArea.value = this.replaceRange(text, textArea.selectionStart, textArea.selectionEnd, insertText);
try {
document.execCommand("ms-endUndoUnit");
- } catch (undefined) {}
+ } catch (error) {}
}
return this.moveCursor(textArea, tag, wrap);
};
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index 533310cc87c..f84a20cf0fe 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -7,6 +7,8 @@
if ((base = w.gl).utils == null) {
base.utils = {};
}
+ // Returns an array containing the value(s) of the
+ // of the key passed as an argument
w.gl.utils.getParameterValues = function(sParam) {
var i, sPageURL, sParameterName, sURLVariables, values;
sPageURL = decodeURIComponent(window.location.search.substring(1));
@@ -23,6 +25,8 @@
}
return values;
};
+ // @param {Object} params - url keys and value to merge
+ // @param {String} url
w.gl.utils.mergeUrlParams = function(params, url) {
var lastChar, newUrl, paramName, paramValue, pattern;
newUrl = decodeURIComponent(url);
@@ -37,12 +41,14 @@
newUrl = "" + newUrl + (newUrl.indexOf('?') > 0 ? '&' : '?') + paramName + "=" + paramValue;
}
}
+ // Remove a trailing ampersand
lastChar = newUrl[newUrl.length - 1];
if (lastChar === '&') {
newUrl = newUrl.slice(0, -1);
}
return newUrl;
};
+ // removes parameter query string from url. returns the modified url
w.gl.utils.removeParamQueryString = function(url, param) {
var urlVariables, variables;
url = decodeURIComponent(url);
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index f145bd3ad74..93daea1dce7 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,17 +1,49 @@
-
+// LineHighlighter
+//
+// Handles single- and multi-line selection and highlight for blob views.
+//
/*= require jquery.scrollTo */
+//
+// ### Example Markup
+//
+// <div id="blob-content-holder">
+// <div class="file-content">
+// <div class="line-numbers">
+// <a href="#L1" id="L1" data-line-number="1">1</a>
+// <a href="#L2" id="L2" data-line-number="2">2</a>
+// <a href="#L3" id="L3" data-line-number="3">3</a>
+// <a href="#L4" id="L4" data-line-number="4">4</a>
+// <a href="#L5" id="L5" data-line-number="5">5</a>
+// </div>
+// <pre class="code highlight">
+// <code>
+// <span id="LC1" class="line">...</span>
+// <span id="LC2" class="line">...</span>
+// <span id="LC3" class="line">...</span>
+// <span id="LC4" class="line">...</span>
+// <span id="LC5" class="line">...</span>
+// </code>
+// </pre>
+// </div>
+// </div>
+//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
this.LineHighlighter = (function() {
+ // CSS class applied to highlighted lines
LineHighlighter.prototype.highlightClass = 'hll';
+ // Internal copy of location.hash so we're not dependent on `location` in tests
LineHighlighter.prototype._hash = '';
function LineHighlighter(hash) {
var range;
if (hash == null) {
+ // Initialize a LineHighlighter object
+ //
+ // hash - String URL hash for dependency injection in tests
hash = location.hash;
}
this.setHash = bind(this.setHash, this);
@@ -24,6 +56,8 @@
if (range[0]) {
this.highlightRange(range);
$.scrollTo("#L" + range[0], {
+ // Scroll to the first highlighted line on initial load
+ // Offset -50 for the sticky top bar, and another -100 for some context
offset: -150
});
}
@@ -32,6 +66,12 @@
LineHighlighter.prototype.bindEvents = function() {
$('#blob-content-holder').on('mousedown', 'a[data-line-number]', this.clickHandler);
+ // While it may seem odd to bind to the mousedown event and then throw away
+ // the click event, there is a method to our madness.
+ //
+ // If not done this way, the line number anchor will sometimes keep its
+ // active state even when the event is cancelled, resulting in an ugly border
+ // around the link and/or a persisted underline text decoration.
return $('#blob-content-holder').on('click', 'a[data-line-number]', function(event) {
return event.preventDefault();
});
@@ -44,6 +84,8 @@
lineNumber = $(event.target).closest('a').data('line-number');
current = this.hashToRange(this._hash);
if (!(current[0] && event.shiftKey)) {
+ // If there's no current selection, or there is but Shift wasn't held,
+ // treat this like a single-line selection.
this.setHash(lineNumber);
return this.highlightLine(lineNumber);
} else if (event.shiftKey) {
@@ -59,10 +101,23 @@
LineHighlighter.prototype.clearHighlight = function() {
return $("." + this.highlightClass).removeClass(this.highlightClass);
+ // Unhighlight previously highlighted lines
};
+ // Convert a URL hash String into line numbers
+ //
+ // hash - Hash String
+ //
+ // Examples:
+ //
+ // hashToRange('#L5') # => [5, null]
+ // hashToRange('#L5-15') # => [5, 15]
+ // hashToRange('#foo') # => [null, null]
+ //
+ // Returns an Array
LineHighlighter.prototype.hashToRange = function(hash) {
var first, last, matches;
+ //?L(\d+)(?:-(\d+))?$/)
matches = hash.match(/^#?L(\d+)(?:-(\d+))?$/);
if (matches && matches.length) {
first = parseInt(matches[1]);
@@ -73,10 +128,16 @@
}
};
+ // Highlight a single line
+ //
+ // lineNumber - Line number to highlight
LineHighlighter.prototype.highlightLine = function(lineNumber) {
return $("#LC" + lineNumber).addClass(this.highlightClass);
};
+ // Highlight all lines within a range
+ //
+ // range - Array containing the starting and ending line numbers
LineHighlighter.prototype.highlightRange = function(range) {
var i, lineNumber, ref, ref1, results;
if (range[1]) {
@@ -90,6 +151,7 @@
}
};
+ // Set the URL hash string
LineHighlighter.prototype.setHash = function(firstLineNumber, lastLineNumber) {
var hash;
if (lastLineNumber) {
@@ -101,10 +163,15 @@
return this.__setLocationHash__(hash);
};
+ // Make the actual hash change in the browser
+ //
+ // This method is stubbed in tests.
LineHighlighter.prototype.__setLocationHash__ = function(value) {
return history.pushState({
turbolinks: false,
url: value
+ // We're using pushState instead of assigning location.hash directly to
+ // prevent the page from scrolling on the hashchange event
}, document.title, value);
};
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 56ebf84c4f6..05644b3d03c 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,10 +1,6 @@
/*= require jquery.waitforimages */
-
-
/*= require task_list */
-
-
/*= require merge_request_tabs */
(function() {
@@ -12,6 +8,11 @@
this.MergeRequest = (function() {
function MergeRequest(opts) {
+ // Initialize MergeRequest behavior
+ //
+ // Options:
+ // action - String, current controller action
+ //
this.opts = opts != null ? opts : {};
this.submitNoteForm = bind(this.submitNoteForm, this);
this.$el = $('.merge-request');
@@ -21,6 +22,7 @@
};
})(this));
this.initTabs();
+ // Prevent duplicate event bindings
this.disableTaskList();
this.initMRBtnListeners();
if ($("a.btn-close").length) {
@@ -28,14 +30,17 @@
}
}
+ // Local jQuery finder
MergeRequest.prototype.$ = function(selector) {
return this.$el.find(selector);
};
MergeRequest.prototype.initTabs = function() {
if (this.opts.action !== 'new') {
+ // `MergeRequests#new` has no tab-persisting or lazy-loading behavior
window.mrTabs = new MergeRequestTabs(this.opts);
} else {
+ // Show the first tab (Commits)
return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
}
};
@@ -96,6 +101,8 @@
url: $('form.js-issuable-update').attr('action'),
data: patchData
});
+ // TODO (rspeicher): Make the merge request description inline-editable like a
+ // note so that we can re-use its form here
};
return MergeRequest;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index ad08209d61e..dcba4a8d275 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,6 +1,49 @@
-
+// MergeRequestTabs
+//
+// Handles persisting and restoring the current tab selection and lazily-loading
+// content on the MergeRequests#show page.
+//
/*= require jquery.cookie */
+//
+// ### Example Markup
+//
+// <ul class="nav-links merge-request-tabs">
+// <li class="notes-tab active">
+// <a data-action="notes" data-target="#notes" data-toggle="tab" href="/foo/bar/merge_requests/1">
+// Discussion
+// </a>
+// </li>
+// <li class="commits-tab">
+// <a data-action="commits" data-target="#commits" data-toggle="tab" href="/foo/bar/merge_requests/1/commits">
+// Commits
+// </a>
+// </li>
+// <li class="diffs-tab">
+// <a data-action="diffs" data-target="#diffs" data-toggle="tab" href="/foo/bar/merge_requests/1/diffs">
+// Diffs
+// </a>
+// </li>
+// </ul>
+//
+// <div class="tab-content">
+// <div class="notes tab-pane active" id="notes">
+// Notes Content
+// </div>
+// <div class="commits tab-pane" id="commits">
+// Commits Content
+// </div>
+// <div class="diffs tab-pane" id="diffs">
+// Diffs Content
+// </div>
+// </div>
+//
+// <div class="mr-loading-status">
+// <div class="loading">
+// Loading Animation
+// </div>
+// </div>
+//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -19,6 +62,7 @@
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
+ // Store the `location` object, allowing for easier stubbing in tests
this._location = location;
this.bindEvents();
this.activateTab(this.opts.action);
@@ -77,6 +121,7 @@
}
};
+ // Activate a tab based on the current action
MergeRequestTabs.prototype.activateTab = function(action) {
if (action === 'show') {
action = 'notes';
@@ -84,20 +129,48 @@
return $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
};
+ // Replaces the current Merge Request-specific action in the URL with a new one
+ //
+ // If the action is "notes", the URL is reset to the standard
+ // `MergeRequests#show` route.
+ //
+ // Examples:
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ // setCurrentAction('diffs')
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('notes')
+ // location.pathname # => "/namespace/project/merge_requests/1"
+ //
+ // location.pathname # => "/namespace/project/merge_requests/1/diffs"
+ // setCurrentAction('commits')
+ // location.pathname # => "/namespace/project/merge_requests/1/commits"
+ //
+ // Returns the new URL String
MergeRequestTabs.prototype.setCurrentAction = function(action) {
var new_state;
+ // Normalize action, just to be safe
if (action === 'show') {
action = 'notes';
}
this.currentAction = action;
+ // Remove a trailing '/commits' or '/diffs'
new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
+ // Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
}
+ // Ensure parameters and hash come along for the ride
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
url: new_state
+ // Replace the current history state with the new one without breaking
+ // Turbolinks' history.
+ //
+ // See https://github.com/rails/turbolinks/issues/363
}, document.title, new_state);
return new_state;
};
@@ -206,6 +279,9 @@
});
};
+ // Show or hide the loading spinner
+ //
+ // status - Boolean, true to show, false to hide
MergeRequestTabs.prototype.toggleLoading = function(status) {
return $('.mr-loading-status .loading').toggle(status);
};
@@ -232,6 +308,7 @@
MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
+ // Returns diff view type
};
MergeRequestTabs.prototype.expandViewContainer = function() {
@@ -245,6 +322,8 @@
if ($gutterIcon.is('.fa-angle-double-right')) {
return $gutterIcon.closest('a').trigger('click', [true]);
}
+ // Wait until listeners are set
+ // Only when sidebar is expanded
}, 0);
};
@@ -259,6 +338,9 @@
return $gutterIcon.closest('a').trigger('click', [true]);
}
}, 0);
+ // Expand the issuable sidebar unless the user explicitly collapsed it
+ // Wait until listeners are set
+ // Only when sidebar is collapsed
};
return MergeRequestTabs;
diff --git a/app/assets/javascripts/merge_request_widget.js b/app/assets/javascripts/merge_request_widget.js
index bd35b6f679d..7bbcdf59838 100644
--- a/app/assets/javascripts/merge_request_widget.js
+++ b/app/assets/javascripts/merge_request_widget.js
@@ -3,6 +3,12 @@
this.MergeRequestWidget = (function() {
function MergeRequestWidget(opts) {
+ // Initialize MergeRequestWidget behavior
+ //
+ // check_enable - Boolean, whether to check automerge status
+ // merge_check_url - String, URL to use to check automerge status
+ // ci_status_url - String, URL to use to check CI status
+ //
this.opts = opts;
$('#modal_merge_info').modal({
show: false
@@ -118,6 +124,8 @@
if (data.coverage) {
_this.showCICoverage(data.coverage);
}
+ // The first check should only update the UI, a notification
+ // should only be displayed on status changes
if (showNotification && !_this.firstCICheck) {
status = _this.ciLabelForStatus(data.status);
if (status === "preparing") {
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index e8d51da7d58..bc1a99057d9 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -110,6 +110,7 @@
},
update: function(event, ui) {
var data;
+ // Prevents sorting from container which element has been removed.
if ($(this).find(ui.item).length > 0) {
data = $(this).sortable("serialize");
return Milestone.sortIssues(data);
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index e897ebdf630..c8031174dd2 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -92,6 +92,7 @@
},
hidden: function() {
$selectbox.hide();
+ // display:block overrides the hide-collapse rule
return $value.css('display', '');
},
clicked: function(selected, $el, e) {
diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch-graph.js
index c0fec1f8607..91132af273a 100644
--- a/app/assets/javascripts/network/branch-graph.js
+++ b/app/assets/javascripts/network/branch-graph.js
@@ -90,6 +90,7 @@
results = [];
while (k < this.mspace) {
this.colors.push(Raphael.getColor(.8));
+ // Skipping a few colors in the spectrum to get more contrast between colors
Raphael.getColor();
Raphael.getColor();
results.push(k++);
@@ -112,6 +113,7 @@
for (mm = j = 0, len = ref.length; j < len; mm = ++j) {
day = ref[mm];
if (cuday !== day[0] || cumonth !== day[1]) {
+ // Dates
r.text(55, this.offsetY + this.unitTime * mm, day[0]).attr({
font: "12px Monaco, monospace",
fill: "#BBB"
@@ -119,6 +121,7 @@
cuday = day[0];
}
if (cumonth !== day[1]) {
+ // Months
r.text(20, this.offsetY + this.unitTime * mm, day[1]).attr({
font: "12px Monaco, monospace",
fill: "#EEE"
@@ -207,6 +210,7 @@
}
r = this.r;
shortrefs = commit.refs;
+ // Truncate if longer than 15 chars
if (shortrefs.length > 17) {
shortrefs = shortrefs.substr(0, 15) + "…";
}
@@ -217,6 +221,7 @@
title: commit.refs
});
textbox = text.getBBox();
+ // Create rectangle based on the size of the textbox
rect = r.rect(x, y - 7, textbox.width + 5, textbox.height + 5, 4).attr({
fill: "#000",
"fill-opacity": .5,
@@ -229,6 +234,7 @@
});
label = r.set(rect, text);
label.transform(["t", -rect.getBBox().width - 15, 0]);
+ // Set text to front
return text.toFront();
};
@@ -283,11 +289,13 @@
parentY = this.offsetY + this.unitTime * parentCommit.time;
parentX1 = this.offsetX + this.unitSpace * (this.mspace - parentCommit.space);
parentX2 = this.offsetX + this.unitSpace * (this.mspace - parent[1]);
+ // Set line color
if (parentCommit.space <= commit.space) {
color = this.colors[commit.space];
} else {
color = this.colors[parentCommit.space];
}
+ // Build line shape
if (parent[1] === commit.space) {
offset = [0, 5];
arrow = "l-2,5,4,0,-2,-5,0,5";
@@ -298,13 +306,17 @@
offset = [-3, 3];
arrow = "l-5,0,2,4,3,-4,-4,2";
}
+ // Start point
route = ["M", x + offset[0], y + offset[1]];
+ // Add arrow if not first parent
if (i > 0) {
route.push(arrow);
}
+ // Circumvent if overlap
if (commit.space !== parentCommit.space || commit.space !== parent[1]) {
route.push("L", parentX2, y + 10, "L", parentX2, parentY - 5);
}
+ // End point
route.push("L", parentX1, parentY);
results.push(r.path(route).attr({
stroke: color,
@@ -325,6 +337,7 @@
"fill-opacity": .5,
stroke: "none"
});
+ // Displayed in the center
return this.element.scrollTop(y - this.graphHeight / 2);
}
};
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 6a7422a7755..67c3e645364 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,4 +1,9 @@
-
+// This is a manifest file that'll be compiled into including all the files listed below.
+// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
+// be included in the compiled file accessible from http://example.com/assets/application.js
+// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
+// the compiled file.
+//
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index d0d5cad813a..c6854f703fb 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,22 +1,10 @@
/*= require autosave */
-
-
/*= require autosize */
-
-
/*= require dropzone */
-
-
/*= require dropzone_input */
-
-
/*= require gfm_auto_complete */
-
-
/*= require jquery.atwho */
-
-
/*= require task_list */
(function() {
@@ -60,26 +48,43 @@
}
Notes.prototype.addBinding = function() {
+ // add note to UI after creation
$(document).on("ajax:success", ".js-main-target-form", this.addNote);
$(document).on("ajax:success", ".js-discussion-note-form", this.addDiscussionNote);
+ // catch note ajax errors
$(document).on("ajax:error", ".js-main-target-form", this.addNoteError);
+ // change note in UI after update
$(document).on("ajax:success", "form.edit-note", this.updateNote);
+ // Edit note link
$(document).on("click", ".js-note-edit", this.showEditForm);
$(document).on("click", ".note-edit-cancel", this.cancelEdit);
+ // Reopen and close actions for Issue/MR combined with note form submit
$(document).on("click", ".js-comment-button", this.updateCloseButton);
$(document).on("keyup input", ".js-note-text", this.updateTargetButtons);
+ // resolve a discussion
$(document).on('click', '.js-comment-resolve-button', this.resolveDiscussion);
+ // remove a note (in general)
$(document).on("click", ".js-note-delete", this.removeNote);
+ // delete note attachment
$(document).on("click", ".js-note-attachment-delete", this.removeAttachment);
+ // reset main target form after submit
$(document).on("ajax:complete", ".js-main-target-form", this.reenableTargetFormSubmitButton);
$(document).on("ajax:success", ".js-main-target-form", this.resetMainTargetForm);
+ // reset main target form when clicking discard
$(document).on("click", ".js-note-discard", this.resetMainTargetForm);
+ // update the file name when an attachment is selected
$(document).on("change", ".js-note-attachment-input", this.updateFormAttachment);
+ // reply to diff/discussion notes
$(document).on("click", ".js-discussion-reply-button", this.replyToDiscussionNote);
+ // add diff note
$(document).on("click", ".js-add-diff-note-button", this.addDiffNote);
+ // hide diff note form
$(document).on("click", ".js-close-discussion-note-form", this.cancelDiscussionForm);
+ // fetch notes when tab becomes visible
$(document).on("visibilitychange", this.visibilityChange);
+ // when issue status changes, we need to refresh data
$(document).on("issuable:change", this.refresh);
+ // when a key is clicked on the notes
return $(document).on("keydown", ".js-note-text", this.keydownNoteText);
};
@@ -112,6 +117,7 @@
return;
}
$textarea = $(e.target);
+ // Edit previous note when UP arrow is hit
switch (e.which) {
case 38:
if ($textarea.val() !== '') {
@@ -123,6 +129,7 @@
return myLastNoteEditBtn.trigger('click', [true, myLastNote]);
}
break;
+ // Cancel creating diff note or editing any note when ESCAPE is hit
case 27:
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
@@ -247,10 +254,13 @@
votesBlock = $('.js-awards-block').eq(0);
gl.awardsHandler.addAwardToEmojiBar(votesBlock, note.name);
return gl.awardsHandler.scrollToAwards();
+ // render note if it not present in loaded list
+ // or skip if rendered
} else if (this.isNewNote(note)) {
this.note_ids.push(note.id);
$notesList = $('ul.main-notes-list');
$notesList.append(note.html).syntaxHighlight();
+ // Update datetime format on the recent note
gl.utils.localTimeAgo($notesList.find("#note_" + note.id + " .js-timeago"), false);
this.initTaskList();
this.refresh();
@@ -291,19 +301,26 @@
row = form.closest("tr");
note_html = $(note.html);
note_html.syntaxHighlight();
+ // is this the first note of discussion?
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
if ((note.original_discussion_id != null) && discussionContainer.length === 0) {
discussionContainer = $(".notes[data-discussion-id='" + note.original_discussion_id + "']");
}
if (discussionContainer.length === 0) {
+ // insert the note and the reply button after the temp row
row.after(note.diff_discussion_html);
+ // remove the note (will be added again below)
row.next().find(".note").remove();
+ // Before that, the container didn't exist
discussionContainer = $(".notes[data-discussion-id='" + note.discussion_id + "']");
+ // Add note to 'Changes' page discussions
discussionContainer.append(note_html);
+ // Init discussion on 'Discussion' page if it is merge request page
if ($('body').attr('data-page').indexOf('projects:merge_request') === 0) {
$('ul.main-notes-list').append(note.discussion_html).syntaxHighlight();
}
} else {
+ // append new note to all matching discussions
discussionContainer.append(note_html);
}
@@ -327,11 +344,18 @@
Notes.prototype.resetMainTargetForm = function(e) {
var form;
form = $(".js-main-target-form");
+ // remove validation errors
form.find(".js-errors").remove();
+ // reset text and preview
form.find(".js-md-write-button").click();
form.find(".js-note-text").val("").trigger("input");
form.find(".js-note-text").data("autosave").reset();
- return this.updateTargetButtons(e);
+
+ var event = document.createEvent('Event');
+ event.initEvent('autosize:update', true, false);
+ form.find('.js-autosize')[0].dispatchEvent(event);
+
+ this.updateTargetButtons(e);
};
Notes.prototype.reenableTargetFormSubmitButton = function() {
@@ -349,9 +373,13 @@
Notes.prototype.setupMainTargetNoteForm = function() {
var form;
+ // find the form
form = $(".js-new-note-form");
+ // Set a global clone of the form for later cloning
this.formClone = form.clone();
+ // show the form
this.setupNoteForm(form);
+ // fix classes
form.removeClass("js-new-note-form");
form.addClass("js-main-target-form");
form.find("#note_line_code").remove();
@@ -416,6 +444,7 @@
}
this.renderDiscussionNote(note);
+ // cleanup after successfully creating a diff/discussion note
this.removeDiscussionNoteForm($form);
};
@@ -428,10 +457,12 @@
Notes.prototype.updateNote = function(_xhr, note, _status) {
var $html, $note_li;
+ // Convert returned HTML to a jQuery object so we can modify it further
$html = $(note.html);
gl.utils.localTimeAgo($('.js-timeago', $html));
$html.syntaxHighlight();
$html.find('.js-task-list-container').taskList('enable');
+ // Find the note's `li` element by ID and replace it with the updated HTML
$note_li = $('.note-row-' + note.id);
$note_li.replaceWith($html);
@@ -456,15 +487,20 @@
note.addClass("is-editting");
form = note.find(".note-edit-form");
form.addClass('current-note-edit-form');
+ // Show the attachment delete link
note.find(".js-note-attachment-delete").show();
done = function($noteText) {
var noteTextVal;
+ // Neat little trick to put the cursor at the end
noteTextVal = $noteText.val();
+ // Store the original note text in a data attribute to retrieve if a user cancels edit.
form.find('form.edit-note').data('original-note', noteTextVal);
return $noteText.val('').val(noteTextVal);
};
new GLForm(form);
if ((scrollTo != null) && (myLastNote != null)) {
+ // scroll to the bottom
+ // so the open of the last element doesn't make a jump
$('html, body').scrollTop($(document).height());
return $('html, body').animate({
scrollTop: myLastNote.offset().top - 150
@@ -500,6 +536,7 @@
form = note.find(".current-note-edit-form");
note.removeClass("is-editting");
form.removeClass("current-note-edit-form");
+ // Replace markdown textarea text with original note text.
return form.find(".js-note-text").val(form.find('form.edit-note').data('original-note'));
};
@@ -515,6 +552,9 @@
var noteId;
noteId = $(e.currentTarget).closest(".note").attr("id");
$(".note[id='" + noteId + "']").each((function(_this) {
+ // A same note appears in the "Discussion" and in the "Changes" tab, we have
+ // to remove all. Using $(".note[id='noteId']") ensure we get all the notes,
+ // where $("#noteId") would return only one.
return function(i, el) {
var note, notes;
note = $(el);
@@ -528,13 +568,17 @@
}
}
+ // check if this is the last note for this line
if (notes.find(".note").length === 1) {
+ // "Discussions" tab
notes.closest(".timeline-entry").remove();
+ // "Changes" tab / commit view
notes.closest("tr").remove();
}
return note.remove();
};
})(this));
+ // Decrement the "Discussions" counter only once
return this.updateNotesCount(-1);
};
@@ -566,10 +610,12 @@
var form, replyLink;
form = this.formClone.clone();
replyLink = $(e.target).closest(".js-discussion-reply-button");
+ // insert the form after the button
replyLink
.closest('.discussion-reply-holder')
.hide()
.after(form);
+ // show the form
return this.setupDiscussionNoteForm(replyLink, form);
};
@@ -584,6 +630,7 @@
*/
Notes.prototype.setupDiscussionNoteForm = function(dataHolder, form) {
+ // setup note target
form.attr('id', "new-discussion-note-form-" + (dataHolder.data("discussionId")));
form.attr("data-line-code", dataHolder.data("lineCode"));
form.find("#note_type").val(dataHolder.data("noteType"));
@@ -631,6 +678,7 @@
addForm = false;
notesContentSelector = ".notes_content";
rowCssToAdd = "<tr class=\"notes_holder js-temp-notes-holder\"><td class=\"notes_line\" colspan=\"2\"></td><td class=\"notes_content\"><div class=\"content\"></div></td></tr>";
+ // In parallel view, look inside the correct left/right pane
if (this.isParallelView()) {
lineType = $link.data("lineType");
notesContentSelector += "." + lineType;
@@ -647,6 +695,7 @@
e.target = replyButton[0];
$.proxy(this.replyToDiscussionNote, replyButton[0], e).call();
} else {
+ // In parallel view, the form may not be present in one of the panes
noteForm = notesContent.find(".js-discussion-note-form");
if (noteForm.length === 0) {
addForm = true;
@@ -654,6 +703,7 @@
}
}
} else {
+ // add a notes row and insert the form
row.after(rowCssToAdd);
nextRow = row.next();
notesContent = nextRow.find(notesContentSelector);
@@ -662,6 +712,7 @@
if (addForm) {
newForm = this.formClone.clone();
newForm.appendTo(notesContent);
+ // show the form
return this.setupDiscussionNoteForm($link, newForm);
}
};
@@ -680,12 +731,15 @@
glForm = form.data('gl-form');
glForm.destroy();
form.find(".js-note-text").data("autosave").reset();
+ // show the reply button (will only work for replies)
form
.prev('.discussion-reply-holder')
.show();
if (row.is(".js-temp-notes-holder")) {
+ // remove temporary row for diff lines
return row.remove();
} else {
+ // only remove the form
return form.remove();
}
};
@@ -707,6 +761,7 @@
Notes.prototype.updateFormAttachment = function() {
var filename, form;
form = $(this).closest("form");
+ // get only the basename
filename = $(this).val().replace(/^.*[\\\/]/, "");
return form.find(".js-attachment-filename").text(filename);
};
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 5fd75799640..5200487814f 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,9 +1,15 @@
+// MarkdownPreview
+//
+// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
+// and showing a warning when more than `x` users are referenced.
+//
(function() {
var lastTextareaPreviewed, markdownPreview, previewButtonSelector, writeButtonSelector;
this.MarkdownPreview = (function() {
function MarkdownPreview() {}
+ // Minimum number of users referenced before triggering a warning
MarkdownPreview.prototype.referenceThreshold = 10;
MarkdownPreview.prototype.ajaxCache = {};
@@ -101,8 +107,10 @@
return;
}
lastTextareaPreviewed = $form.find('textarea.markdown-area');
+ // toggle tabs
$form.find(writeButtonSelector).parent().removeClass('active');
$form.find(previewButtonSelector).parent().addClass('active');
+ // toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
return markdownPreview.showPreview($form);
@@ -113,8 +121,10 @@
return;
}
lastTextareaPreviewed = null;
+ // toggle tabs
$form.find(writeButtonSelector).parent().addClass('active');
$form.find(previewButtonSelector).parent().removeClass('active');
+ // toggle content
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
return $form.find('.md-preview-holder').hide();
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js
index a3eea316f67..30cd6f6e470 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js
@@ -5,6 +5,7 @@
GitLabCrop = (function() {
var FILENAMEREGEX;
+ // Matches everything but the file name
FILENAMEREGEX = /^.*[\\\/]/;
function GitLabCrop(input, opts) {
@@ -17,11 +18,18 @@
this.onModalShow = bind(this.onModalShow, this);
this.onPickImageClick = bind(this.onPickImageClick, this);
this.fileInput = $(input);
+ // We should rename to avoid spec to fail
+ // Form will submit the proper input filed with a file using FormData
this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
+ // Set defaults
this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
+ // Required params
+ // Ensure needed elements are jquery objects
+ // If selector is provided we will convert them to a jQuery Object
this.filename = this.getElement(this.filename);
this.previewImage = this.getElement(this.previewImage);
this.pickImageEl = this.getElement(this.pickImageEl);
+ // Modal elements usually are outside the @form element
this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
@@ -93,8 +101,8 @@
return this.modalCropImg.attr('src', '').cropper('destroy');
};
- GitLabCrop.prototype.onUploadImageBtnClick = function(e) {
- e.preventDefault();
+ GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image
+ e.preventDefault(); // Destroy cropper instance
this.setBlob();
this.setPreview();
this.modalCrop.modal('hide');
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index ed1d87abafe..60f9fba5777 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -11,9 +11,11 @@
this.form = (ref = opts.form) != null ? ref : $('.edit-user');
$('.js-preferences-form').on('change.preference', 'input[type=radio]', function() {
return $(this).parents('form').submit();
+ // Automatically submit the Preferences form when any of its radio buttons change
});
$('#user_notification_email').on('change', function() {
return $(this).parents('form').submit();
+ // Automatically submit email form when it changes
});
$('.update-username').on('ajax:before', function() {
$('.loading-username').show();
@@ -76,6 +78,7 @@
},
complete: function() {
window.scrollTo(0, 0);
+ // Enable submit button after requests ends
return self.form.find(':input[disabled]').enable();
}
});
@@ -93,6 +96,7 @@
if (comment && comment.length > 1 && $title.val() === '') {
return $title.val(comment[1]).change();
}
+ // Extract the SSH Key title from its comment
});
if (gl.utils.getPagePath() === 'profiles') {
return new Profile();
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index b95faadc8e7..d6e4d9f7ad8 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index 66e097c0a28..a6c015299a0 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -11,7 +11,13 @@
url = $("#project_clone").val();
$('#project_clone').val(url);
return $('.clone').text(url);
+ // Git protocol switcher
+ // Remove the active class for all buttons (ssh, http, kerberos if shown)
+ // Add the active class for the clicked button
+ // Update the input field
+ // Update the command line instructions
});
+ // Ref switcher
this.initRefSwitcher();
$('.project-refs-select').on('change', function() {
return $(this).parents('form').submit();
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index 4925f0519f0..5bf900f3e1d 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -13,8 +13,11 @@
this.selectRowUp = bind(this.selectRowUp, this);
this.filePaths = {};
this.inputElement = this.element.find(".file-finder-input");
+ // init event
this.initEvent();
+ // focus text input box
this.inputElement.focus();
+ // load file list
this.load(this.options.url);
}
@@ -42,6 +45,7 @@
}
}
});
+ // init event
};
ProjectFindFile.prototype.findFile = function() {
@@ -49,8 +53,10 @@
searchText = this.inputElement.val();
result = searchText.length > 0 ? fuzzaldrinPlus.filter(this.filePaths, searchText) : this.filePaths;
return this.renderList(result, searchText);
+ // find file
};
+ // files pathes load
ProjectFindFile.prototype.load = function(url) {
return $.ajax({
url: url,
@@ -67,6 +73,7 @@
});
};
+ // render result
ProjectFindFile.prototype.renderList = function(filePaths, searchText) {
var blobItemUrl, filePath, html, i, j, len, matches, results;
this.element.find(".tree-table > tbody").empty();
@@ -86,6 +93,7 @@
return results;
};
+ // highlight text(awefwbwgtc -> <b>a</b>wefw<b>b</b>wgt<b>c</b> )
highlighter = function(element, text, matches) {
var highlightText, j, lastIndex, len, matchIndex, matchedChars, unmatched;
lastIndex = 0;
@@ -110,6 +118,7 @@
return element.append(document.createTextNode(text.substring(lastIndex)));
};
+ // make tbody row html
ProjectFindFile.prototype.makeHtml = function(filePath, matches, blobItemUrl) {
var $tr;
$tr = $("<tr class='tree-item'><td class='tree-item-file-name'><i class='fa fa-file-text-o fa-fw'></i><span class='str-truncated'><a></a></span></td></tr>");
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 798f15e40a0..a787b11f2a9 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -4,6 +4,8 @@
this.ProjectNew = (function() {
function ProjectNew() {
this.toggleSettings = bind(this.toggleSettings, this);
+ this.$selects = $('.features select');
+
$('.project-edit-container').on('ajax:before', (function(_this) {
return function() {
$('.project-edit-container').hide();
@@ -15,18 +17,24 @@
}
ProjectNew.prototype.toggleSettings = function() {
- this._showOrHide('#project_builds_enabled', '.builds-feature');
- return this._showOrHide('#project_merge_requests_enabled', '.merge-requests-feature');
+ var self = this;
+
+ this.$selects.each(function () {
+ var $select = $(this),
+ className = $select.data('field').replace(/_/g, '-')
+ .replace('access-level', 'feature');
+ self._showOrHide($select, '.' + className);
+ });
};
ProjectNew.prototype.toggleSettingsOnclick = function() {
- return $('#project_builds_enabled, #project_merge_requests_enabled').on('click', this.toggleSettings);
+ this.$selects.on('change', this.toggleSettings);
};
ProjectNew.prototype._showOrHide = function(checkElement, container) {
- var $container;
- $container = $(container);
- if ($(checkElement).prop('checked')) {
+ var $container = $(container);
+
+ if ($(checkElement).val() !== '0') {
return $container.show();
} else {
return $container.hide();
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index 8ca4c427912..c8cfc9a9ba8 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -7,3 +7,5 @@
})();
}).call(this);
+
+// I kept class for future
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 4f415b05dbc..04fb49552e8 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -33,6 +33,7 @@
$('.projects-list-holder').replaceWith(data.html);
return history.replaceState({
page: project_filter_url
+ // Change url so if user reload a page - search results are saved
}, document.title, project_filter_url);
},
dataType: "json"
diff --git a/app/assets/javascripts/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branch_dropdown.js.es6
index 6738dc8862d..983322cbecc 100644
--- a/app/assets/javascripts/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branch_dropdown.js.es6
@@ -45,6 +45,7 @@ class ProtectedBranchDropdown {
}
onClickCreateWildcard() {
+ // Refresh the dropdown's data, which ends up calling `getProtectedBranches`
this.$dropdown.data('glDropdown').remote.execute();
this.$dropdown.data('glDropdown').selectRowAtIndex(0);
}
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 227e8c696b4..8abb09c626f 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -24,6 +24,7 @@
this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this);
this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this);
this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || '';
+ // Dropdown Element
this.dropdown = this.wrap.find('.dropdown');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
@@ -35,6 +36,7 @@
this.repositoryInputEl = this.getElement('#repository_ref');
this.clearInput = this.getElement('.js-clear-input');
this.saveOriginalState();
+ // Only when user is logged in
if (gon.current_user_id) {
this.createAutocomplete();
}
@@ -43,6 +45,7 @@
this.bindEvents();
}
+ // Finds an element inside wrapper element
SearchAutocomplete.prototype.getElement = function(selector) {
return this.wrap.find(selector);
};
@@ -82,6 +85,7 @@
}
return;
}
+ // Prevent multiple ajax calls
if (this.loadingSuggestions) {
return;
}
@@ -92,14 +96,17 @@
term: term
}, function(response) {
var data, firstCategory, i, lastCategory, len, suggestion;
+ // Hide dropdown menu if no suggestions returns
if (!response.length) {
_this.disableAutocomplete();
return;
}
data = [];
+ // List results
firstCategory = true;
for (i = 0, len = response.length; i < len; i++) {
suggestion = response[i];
+ // Add group header before list each group
if (lastCategory !== suggestion.category) {
if (!firstCategory) {
data.push('separator');
@@ -119,6 +126,7 @@
url: suggestion.url
});
}
+ // Add option to proceed with the search
if (data.length) {
data.push('separator');
data.push({
@@ -169,11 +177,13 @@
SearchAutocomplete.prototype.serializeState = function() {
return {
+ // Search Criteria
search_project_id: this.projectInputEl.val(),
group_id: this.groupInputEl.val(),
search_code: this.searchCodeInputEl.val(),
repository_ref: this.repositoryInputEl.val(),
scope: this.scopeInputEl.val(),
+ // Location badge
_location: this.locationBadgeEl.text()
};
};
@@ -194,6 +204,7 @@
SearchAutocomplete.prototype.enableAutocomplete = function() {
var _this;
+ // No need to enable anything if user is not logged in
if (!gon.current_user_id) {
return;
}
@@ -206,18 +217,22 @@
};
SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
+ // Saves last length of the entered text
return this.saveTextLength();
};
SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
+ // when trying to remove the location badge
if (this.lastTextLength === 0 && this.badgePresent()) {
this.removeLocationBadge();
}
+ // When removing the last character and no badge is present
if (this.lastTextLength === 1) {
this.disableAutocomplete();
}
+ // When removing any character from existin value
if (this.lastTextLength > 1) {
this.enableAutocomplete();
}
@@ -232,9 +247,12 @@
case KEYCODE.DOWN:
return;
default:
+ // Handle the case when deleting the input value other than backspace
+ // e.g. Pressing ctrl + backspace or ctrl + x
if (this.searchInput.val() === '') {
this.disableAutocomplete();
} else {
+ // We should display the menu only when input is not empty
if (e.keyCode !== KEYCODE.ENTER) {
this.enableAutocomplete();
}
@@ -243,7 +261,9 @@
this.wrap.toggleClass('has-value', !!e.target.value);
};
+ // Avoid falsy value to be returned
SearchAutocomplete.prototype.onSearchInputClick = function(e) {
+ // Prevents closing the dropdown menu
return e.stopImmediatePropagation();
};
@@ -267,6 +287,7 @@
SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
+ // If input is blank then restore state
if (this.searchInput.val() === '') {
return this.restoreOriginalState();
}
@@ -311,6 +332,7 @@
results = [];
for (i = 0, len = inputs.length; i < len; i++) {
input = inputs[i];
+ // _location isnt a input
if (input === '_location') {
break;
}
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 3b28332854a..3aa8536d40a 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -86,6 +86,7 @@
var defaultStopCallback;
defaultStopCallback = Mousetrap.stopCallback;
return function(e, element, combo) {
+ // allowed shortcuts if textarea, input, contenteditable are focused
if (['ctrl+shift+p', 'command+shift+p'].indexOf(combo) !== -1) {
return false;
} else {
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 6c78914d338..92ce31969e3 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -14,8 +14,10 @@
ShortcutsFindFile.__super__.constructor.call(this);
_oldStopCallback = Mousetrap.stopCallback;
Mousetrap.stopCallback = (function(_this) {
+ // override to fire shortcuts action when focus in textbox
return function(event, element, combo) {
if (element === _this.projectFindFile.inputElement[0] && (combo === 'up' || combo === 'down' || combo === 'esc' || combo === 'enter')) {
+ // when press up/down key in textbox, cusor prevent to move to home/end
event.preventDefault();
return false;
}
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 3f3a8a9dfd9..235bf4f95ec 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,7 +1,5 @@
/*= require mousetrap */
-
-
/*= require shortcuts_navigation */
(function() {
@@ -43,16 +41,20 @@
if (selected.trim() === "") {
return;
}
+ // Put a '>' character before each non-empty line in the selection
quote = _.map(selected.split("\n"), function(val) {
if (val.trim() !== '') {
return "> " + val + "\n";
}
});
+ // If replyField already has some content, add a newline before our quote
separator = replyField.val().trim() !== "" && "\n" || '';
replyField.val(function(_, current) {
return current + separator + quote.join('') + "\n";
});
+ // Trigger autosave for the added text
replyField.trigger('input');
+ // Focus the input field
return replyField.focus();
}
};
diff --git a/app/assets/javascripts/sidebar.js b/app/assets/javascripts/sidebar.js
deleted file mode 100644
index bd0c1194b36..00000000000
--- a/app/assets/javascripts/sidebar.js
+++ /dev/null
@@ -1,41 +0,0 @@
-(function() {
- var collapsed, expanded, toggleSidebar;
-
- collapsed = 'page-sidebar-collapsed';
-
- expanded = 'page-sidebar-expanded';
-
- toggleSidebar = function() {
- $('.page-with-sidebar').toggleClass(collapsed + " " + expanded);
- $('.navbar-fixed-top').toggleClass("header-collapsed header-expanded");
- if ($.cookie('pin_nav') === 'true') {
- $('.navbar-fixed-top').toggleClass('header-pinned-nav');
- $('.page-with-sidebar').toggleClass('page-sidebar-pinned');
- }
- return setTimeout((function() {
- var niceScrollBars;
- niceScrollBars = $('.nav-sidebar').niceScroll();
- return niceScrollBars.updateScrollBar();
- }), 300);
- };
-
- $(document).off('click', 'body').on('click', 'body', function(e) {
- var $nav, $target, $toggle, pageExpanded;
- if ($.cookie('pin_nav') !== 'true') {
- $target = $(e.target);
- $nav = $target.closest('.sidebar-wrapper');
- pageExpanded = $('.page-with-sidebar').hasClass('page-sidebar-expanded');
- $toggle = $target.closest('.toggle-nav-collapse, .side-nav-toggle');
- if ($nav.length === 0 && pageExpanded && $toggle.length === 0) {
- $('.page-with-sidebar').toggleClass('page-sidebar-collapsed page-sidebar-expanded');
- return $('.navbar-fixed-top').toggleClass('header-collapsed header-expanded');
- }
- }
- });
-
- $(document).on("click", '.toggle-nav-collapse, .side-nav-toggle', function(e) {
- e.preventDefault();
- return toggleSidebar();
- });
-
-}).call(this);
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
new file mode 100644
index 00000000000..755fac8107b
--- /dev/null
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -0,0 +1,93 @@
+((global) => {
+ let singleton;
+
+ const pinnedStateCookie = 'pin_nav';
+ const sidebarBreakpoint = 1024;
+
+ const pageSelector = '.page-with-sidebar';
+ const navbarSelector = '.navbar-fixed-top';
+ const sidebarWrapperSelector = '.sidebar-wrapper';
+ const sidebarContentSelector = '.nav-sidebar';
+
+ const pinnedToggleSelector = '.js-nav-pin';
+ const sidebarToggleSelector = '.toggle-nav-collapse, .side-nav-toggle';
+
+ const pinnedPageClass = 'page-sidebar-pinned';
+ const expandedPageClass = 'page-sidebar-expanded';
+
+ const pinnedNavbarClass = 'header-sidebar-pinned';
+ const expandedNavbarClass = 'header-sidebar-expanded';
+
+ class Sidebar {
+ constructor() {
+ if (!singleton) {
+ singleton = this;
+ singleton.init();
+ }
+ return singleton;
+ }
+
+ init() {
+ this.isPinned = $.cookie(pinnedStateCookie) === 'true';
+ this.isExpanded = (
+ window.innerWidth >= sidebarBreakpoint &&
+ $(pageSelector).hasClass(expandedPageClass)
+ );
+ $(document)
+ .on('click', sidebarToggleSelector, () => this.toggleSidebar())
+ .on('click', pinnedToggleSelector, () => this.togglePinnedState())
+ .on('click', 'html, body', (e) => this.handleClickEvent(e))
+ .on('page:change', () => this.renderState());
+ this.renderState();
+ }
+
+ handleClickEvent(e) {
+ if (this.isExpanded && (!this.isPinned || window.innerWidth < sidebarBreakpoint)) {
+ const $target = $(e.target);
+ const targetIsToggle = $target.closest(sidebarToggleSelector).length > 0;
+ const targetIsSidebar = $target.closest(sidebarWrapperSelector).length > 0;
+ if (!targetIsToggle && (!targetIsSidebar || $target.closest('a'))) {
+ this.toggleSidebar();
+ }
+ }
+ }
+
+ toggleSidebar() {
+ this.isExpanded = !this.isExpanded;
+ this.renderState();
+ }
+
+ togglePinnedState() {
+ this.isPinned = !this.isPinned;
+ if (!this.isPinned) {
+ this.isExpanded = false;
+ }
+ $.cookie(pinnedStateCookie, this.isPinned ? 'true' : 'false', {
+ path: gon.relative_url_root || '/',
+ expires: 3650
+ });
+ this.renderState();
+ }
+
+ renderState() {
+ $(pageSelector)
+ .toggleClass(pinnedPageClass, this.isPinned && this.isExpanded)
+ .toggleClass(expandedPageClass, this.isExpanded);
+ $(navbarSelector)
+ .toggleClass(pinnedNavbarClass, this.isPinned && this.isExpanded)
+ .toggleClass(expandedNavbarClass, this.isExpanded);
+
+ const $pinnedToggle = $(pinnedToggleSelector);
+ const tooltipText = this.isPinned ? 'Unpin navigation' : 'Pin navigation';
+ const tooltipState = $pinnedToggle.attr('aria-describedby') && this.isExpanded ? 'show' : 'hide';
+ $pinnedToggle.attr('title', tooltipText).tooltip('fixTitle').tooltip(tooltipState);
+
+ if (this.isExpanded) {
+ setTimeout(() => $(sidebarContentSelector).niceScroll().updateScrollBar(), 200);
+ }
+ }
+ }
+
+ global.Sidebar = Sidebar;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
new file mode 100644
index 00000000000..6f0996c0d2a
--- /dev/null
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -0,0 +1,11 @@
+(global => {
+ global.gl = global.gl || {};
+
+ gl.SnippetsList = function() {
+ var $holder = $('.snippets-list-holder');
+
+ $holder.find('.pagination').on('ajax:success', (e, data) => {
+ $holder.replaceWith(data.html);
+ });
+ }
+})(window);
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index dba62638c78..2ae7bf5fc15 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,9 +1,20 @@
+// Syntax Highlighter
+//
+// Applies a syntax highlighting color scheme CSS class to any element with the
+// `js-syntax-highlight` class
+//
+// ### Example Markup
+//
+// <div class="js-syntax-highlight"></div>
+//
(function() {
$.fn.syntaxHighlight = function() {
var $children;
if ($(this).hasClass('js-syntax-highlight')) {
+ // Given the element itself, apply highlighting
return $(this).addClass(gon.user_color_scheme);
} else {
+ // Given a parent element, recurse to any of its applicable children
$children = $(this).find('.js-syntax-highlight');
if ($children.length) {
return $children.syntaxHighlight();
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js
index 23eda7d44ca..93421649ac7 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js
@@ -129,16 +129,21 @@
var currPage, currPages, newPages, pageParams, url;
currPages = this.getTotalPages();
currPage = this.getCurrentPage();
+ // Refresh if no remaining Todos
if (!total) {
location.reload();
return;
}
+ // Do nothing if no pagination
if (!currPages) {
return;
}
newPages = Math.ceil(total / this.getTodosPerPage());
+ // Includes query strings
url = location.href;
+ // If new total of pages is different than we have now
if (newPages !== currPages) {
+ // Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) {
pageParams = {
page: currPages - 1
@@ -155,6 +160,7 @@
if (!todoLink) {
return;
}
+ // Allow Meta-Click or Mouse3-click to open in a new tab
if (e.metaKey || e.which === 2) {
e.preventDefault();
return window.open(todoLink, '_blank');
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 78e159a7ed9..9b7be17c4fe 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -2,6 +2,8 @@
this.TreeView = (function() {
function TreeView() {
this.initKeyNav();
+ // Code browser tree slider
+ // Make the entire tree-item row clickable, but not if clicking another link (like a commit message)
$(".tree-content-holder .tree-item").on('click', function(e) {
var $clickedEl, path;
$clickedEl = $(e.target);
@@ -15,6 +17,7 @@
}
}
});
+ // Show the "Loading commit data" for only the first element
$('span.log_loading:first').removeClass('hide');
}
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index 9ba847fb0c2..ce2930c7fc7 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,3 +1,7 @@
+// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> authenticated -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -15,6 +19,17 @@
this.appId = u2fParams.app_id;
this.challenge = u2fParams.challenge;
this.signRequests = u2fParams.sign_requests.map(function(request) {
+ // The U2F Javascript API v1.1 requires a single challenge, with
+ // _no challenges per-request_. The U2F Javascript API v1.0 requires a
+ // challenge per-request, which is done by copying the single challenge
+ // into every request.
+ //
+ // In either case, we don't need the per-request challenges that the server
+ // has generated, so we can remove them.
+ //
+ // Note: The server library fixes this behaviour in (unreleased) version 1.0.0.
+ // This can be removed once we upgrade.
+ // https://github.com/castle/ruby-u2f/commit/103f428071a81cd3d5f80c2e77d522d5029946a4
return _(request).omit('challenge');
});
}
@@ -41,6 +56,7 @@
})(this), 10);
};
+ // Rendering #
U2FAuthenticate.prototype.templates = {
"notSupported": "#js-authenticate-u2f-not-supported",
"setup": '#js-authenticate-u2f-setup',
@@ -75,6 +91,8 @@
U2FAuthenticate.prototype.renderAuthenticated = function(deviceResponse) {
this.renderTemplate('authenticated');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
return this.container.find("#js-device-response").val(deviceResponse);
};
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index c87e0840df3..926912fa988 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,3 +1,7 @@
+// Register U2F (universal 2nd factor) devices for users to authenticate with.
+//
+// State Flow #1: setup -> in_progress -> registered -> POST to server
+// State Flow #2: setup -> in_progress -> error -> setup
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -39,6 +43,7 @@
})(this), 10);
};
+ // Rendering #
U2FRegister.prototype.templates = {
"notSupported": "#js-register-u2f-not-supported",
"setup": '#js-register-u2f-setup',
@@ -73,6 +78,8 @@
U2FRegister.prototype.renderRegistered = function(deviceResponse) {
this.renderTemplate('registered');
+ // Prefer to do this instead of interpolating using Underscore templates
+ // because of JSON escaping issues.
return this.container.find("#js-device-response").val(deviceResponse);
};
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
index e5e75701fee..8a657780eb6 100644
--- a/app/assets/javascripts/user_tabs.js
+++ b/app/assets/javascripts/user_tabs.js
@@ -1,3 +1,61 @@
+// UserTabs
+//
+// Handles persisting and restoring the current tab selection and lazily-loading
+// content on the Users#show page.
+//
+// ### Example Markup
+//
+// <ul class="nav-links">
+// <li class="activity-tab active">
+// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+// Activity
+// </a>
+// </li>
+// <li class="groups-tab">
+// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+// Groups
+// </a>
+// </li>
+// <li class="contributed-tab">
+// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
+// Contributed projects
+// </a>
+// </li>
+// <li class="projects-tab">
+// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
+// Personal projects
+// </a>
+// </li>
+// <li class="snippets-tab">
+// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
+// </a>
+// </li>
+// </ul>
+//
+// <div class="tab-content">
+// <div class="tab-pane" id="activity">
+// Activity Content
+// </div>
+// <div class="tab-pane" id="groups">
+// Groups Content
+// </div>
+// <div class="tab-pane" id="contributed">
+// Contributed projects content
+// </div>
+// <div class="tab-pane" id="projects">
+// Projects content
+// </div>
+// <div class="tab-pane" id="snippets">
+// Snippets content
+// </div>
+// </div>
+//
+// <div class="loading-status">
+// <div class="loading">
+// Loading Animation
+// </div>
+// </div>
+//
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -6,18 +64,23 @@
this.tabShown = bind(this.tabShown, this);
var i, item, len, ref, ref1, ref2, ref3;
this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document);
+ // Make jQuery object if selector is provided
if (typeof this.parentEl === 'string') {
this.parentEl = $(this.parentEl);
}
+ // Store the `location` object, allowing for easier stubbing in tests
this._location = location;
+ // Set tab states
this.loaded = {};
ref3 = this.parentEl.find('.nav-links a');
for (i = 0, len = ref3.length; i < len; i++) {
item = ref3[i];
this.loaded[$(item).attr('data-action')] = false;
}
+ // Actions
this.actions = Object.keys(this.loaded);
this.bindEvents();
+ // Set active tab
if (this.action === 'show') {
this.action = this.defaultAction;
}
@@ -25,6 +88,7 @@
}
UserTabs.prototype.bindEvents = function() {
+ // Toggle event listeners
return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown);
};
@@ -74,6 +138,7 @@
tabSelector = 'div#' + action;
_this.parentEl.find(tabSelector).html(data.html);
_this.loaded[action] = true;
+ // Fix tooltips
return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
};
})(this)
@@ -97,13 +162,17 @@
UserTabs.prototype.setCurrentAction = function(action) {
var new_state, regExp;
+ // Remove possible actions from URL
regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$');
new_state = this._location.pathname;
+ // remove trailing slashes
new_state = new_state.replace(/\/+$/, "");
new_state = new_state.replace(regExp, '');
+ // Append the new action if we're on a tab other than 'activity'
if (action !== this.defaultAction) {
new_state += "/" + action;
}
+ // Ensure parameters and hash come along for the ride
new_state += this._location.search + this._location.hash;
history.replaceState({
turbolinks: true,
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 74ecf4f4cf9..b8da7c4f297 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -11,6 +11,8 @@
this.daySizeWithSpace = this.daySize + (this.daySpace * 2);
this.monthNames = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
this.months = [];
+ // Loop through the timestamps to create a group of objects
+ // The group of objects will be grouped based on the day of the week they are
this.timestampsTmp = [];
var group = 0;
@@ -29,12 +31,15 @@
var day = date.getDay();
var count = timestamps[date.getTime() * 0.001];
+ // Create a new group array if this is the first day of the week
+ // or if is first object
if ((day === 0 && i !== 0) || i === 0) {
this.timestampsTmp.push([]);
group++;
}
var innerArray = this.timestampsTmp[group - 1];
+ // Push to the inner array the values that will be used to render map
innerArray.push({
count: count || 0,
date: date,
@@ -42,8 +47,10 @@
});
}
+ // Init color functions
this.colorKey = this.initColorKey();
this.color = this.initColor();
+ // Init the svg element
this.renderSvg(group);
this.renderDays();
this.renderMonths();
@@ -52,8 +59,22 @@
this.initTooltips();
}
+ // Add extra padding for the last month label if it is also the last column
+ Calendar.prototype.getExtraWidthPadding = function(group) {
+ var extraWidthPadding = 0;
+ var lastColMonth = this.timestampsTmp[group - 1][0].date.getMonth();
+ var secondLastColMonth = this.timestampsTmp[group - 2][0].date.getMonth();
+
+ if (lastColMonth != secondLastColMonth) {
+ extraWidthPadding = 3;
+ }
+
+ return extraWidthPadding;
+ }
+
Calendar.prototype.renderSvg = function(group) {
- return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', (group + 1) * this.daySizeWithSpace).attr('height', 167).attr('class', 'contrib-calendar');
+ var width = (group + 1) * this.daySizeWithSpace + this.getExtraWidthPadding(group);
+ return this.svg = d3.select('.js-contrib-calendar').append('svg').attr('width', width).attr('height', 167).attr('class', 'contrib-calendar');
};
Calendar.prototype.renderDays = function() {
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index b95faadc8e7..d6e4d9f7ad8 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -3,5 +3,4 @@
(function() {
-
}).call(this);
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index bad82868ab0..9c277998db4 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -81,6 +81,7 @@
if (term.length === 0) {
showDivider = 0;
if (firstUser) {
+ // Move current user to the front of the list
for (index = j = 0, len = users.length; j < len; index = ++j) {
obj = users[index];
if (obj.username === firstUser) {
@@ -115,6 +116,7 @@
if (showDivider) {
users.splice(showDivider, 0, "divider");
}
+ // Send the data back
return callback(users);
});
},
@@ -139,6 +141,7 @@
inputId: 'issue_assignee_id',
hidden: function(e) {
$selectbox.hide();
+ // display:block overrides the hide-collapse rule
return $value.css('display', '');
},
clicked: function(user, $el, e) {
@@ -177,6 +180,7 @@
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='30' />";
}
}
+ // split into three parts so we can remove the username section if nessesary
listWithName = "<li> <a href='#' class='dropdown-menu-user-link " + selected + "'> " + img + " <strong class='dropdown-menu-user-full-name'> " + user.name + " </strong>";
listWithUserName = "<span class='dropdown-menu-user-username'> " + username + " </span>";
listClosingTags = "</a> </li>";
@@ -215,6 +219,7 @@
};
if (query.term.length === 0) {
if (firstUser) {
+ // Move current user to the front of the list
ref = data.results;
for (index = j = 0, len = ref.length; j < len; index = ++j) {
obj = ref[index];
@@ -271,6 +276,7 @@
return _this.formatSelection.apply(_this, args);
},
dropdownCssClass: "ajax-users-dropdown",
+ // we do not want to escape markup since we are displaying html in results
escapeMarkup: function(m) {
return m;
}
@@ -318,6 +324,8 @@
});
};
+ // Return users list. Filtered by query
+ // Only active users retrieved
UsersSelect.prototype.users = function(query, options, callback) {
var url;
url = this.buildUrl(this.usersPath);
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 71236c6a27d..777b32b41c9 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,21 +1,34 @@
-
+// Zen Mode (full screen) textarea
+//
/*= provides zen_mode:enter */
-
-
/*= provides zen_mode:leave */
-
-
+//
/*= require jquery.scrollTo */
-
-
/*= require dropzone */
-
-
/*= require mousetrap */
-
-
/*= require mousetrap/pause */
+//
+// ### Events
+//
+// `zen_mode:enter`
+//
+// Fired when the "Edit in fullscreen" link is clicked.
+//
+// **Synchronicity** Sync
+// **Bubbles** Yes
+// **Cancelable** No
+// **Target** a.js-zen-enter
+//
+// `zen_mode:leave`
+//
+// Fired when the "Leave Fullscreen" link is clicked.
+//
+// **Synchronicity** Sync
+// **Bubbles** Yes
+// **Cancelable** No
+// **Target** a.js-zen-leave
+//
(function() {
this.ZenMode = (function() {
function ZenMode() {
@@ -40,6 +53,7 @@
};
})(this));
$(document).on('keydown', function(e) {
+ // Esc
if (e.keyCode === 27) {
e.preventDefault();
return $(document).trigger('zen_mode:leave');
@@ -52,6 +66,7 @@
this.active_backdrop = $(backdrop);
this.active_backdrop.addClass('fullscreen');
this.active_textarea = this.active_backdrop.find('textarea');
+ // Prevent a user-resized textarea from persisting to fullscreen
this.active_textarea.removeAttr('style');
return this.active_textarea.focus();
};
diff --git a/app/assets/stylesheets/framework/animations.scss b/app/assets/stylesheets/framework/animations.scss
index 1fec61bdba1..1e9a45c19b8 100644
--- a/app/assets/stylesheets/framework/animations.scss
+++ b/app/assets/stylesheets/framework/animations.scss
@@ -8,65 +8,44 @@
// Copyright (c) 2016 Daniel Eden
.animated {
- -webkit-animation-duration: 1s;
- animation-duration: 1s;
- -webkit-animation-fill-mode: both;
- animation-fill-mode: both;
-}
-
-.animated.infinite {
- -webkit-animation-iteration-count: infinite;
- animation-iteration-count: infinite;
-}
+ @include webkit-prefix(animation-duration, 1s);
+ @include webkit-prefix(animation-fill-mode, both);
-.animated.hinge {
- -webkit-animation-duration: 2s;
- animation-duration: 2s;
-}
+ &.infinite {
+ @include webkit-prefix(animation-iteration-count, infinite);
+ }
-.animated.flipOutX,
-.animated.flipOutY,
-.animated.bounceIn,
-.animated.bounceOut {
- -webkit-animation-duration: .75s;
- animation-duration: .75s;
-}
+ &.once {
+ @include webkit-prefix(animation-iteration-count, 1);
+ }
-@-webkit-keyframes pulse {
- from {
- -webkit-transform: scale3d(1, 1, 1);
- transform: scale3d(1, 1, 1);
+ &.hinge {
+ @include webkit-prefix(animation-duration, 2s);
}
- 50% {
- -webkit-transform: scale3d(1.05, 1.05, 1.05);
- transform: scale3d(1.05, 1.05, 1.05);
+ &.flipOutX,
+ &.flipOutY,
+ &.bounceIn,
+ &.bounceOut {
+ @include webkit-prefix(animation-duration, .75s);
}
- to {
- -webkit-transform: scale3d(1, 1, 1);
- transform: scale3d(1, 1, 1);
+ &.short {
+ @include webkit-prefix(animation-duration, 321ms);
+ @include webkit-prefix(animation-fill-mode, none);
}
}
-@keyframes pulse {
- from {
- -webkit-transform: scale3d(1, 1, 1);
- transform: scale3d(1, 1, 1);
+@include keyframes(pulse) {
+ from, to {
+ @include webkit-prefix(transform, scale3d(1, 1, 1));
}
50% {
- -webkit-transform: scale3d(1.05, 1.05, 1.05);
- transform: scale3d(1.05, 1.05, 1.05);
- }
-
- to {
- -webkit-transform: scale3d(1, 1, 1);
- transform: scale3d(1, 1, 1);
+ @include webkit-prefix(transform, scale3d(1.05, 1.05, 1.05));
}
}
.pulse {
- -webkit-animation-name: pulse;
- animation-name: pulse;
+ @include webkit-prefix(animation-name, pulse);
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 7ce203d2ec7..f5223207f3a 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -249,6 +249,10 @@
> .controls {
float: right;
}
+
+ .new-branch {
+ margin-top: 3px;
+ }
}
.content-block-small {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index cd3ddf5fee9..4618687a4be 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -206,7 +206,9 @@
}
svg, .fa {
- margin-right: 3px;
+ &:not(:last-child) {
+ margin-right: 3px;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 8984bce616c..5957dce89bc 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -53,7 +53,7 @@ pre {
&.well-pre {
border: 1px solid #eee;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #555;
}
@@ -225,7 +225,7 @@ li.note {
.milestone {
&.milestone-closed {
- background: #f9f9f9;
+ background: $gray-light;
}
.progress {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index d3e3fc50736..76a3c083697 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -63,7 +63,7 @@
&.image_file {
background: #eee;
text-align: center;
-
+
img {
padding: 20px;
max-width: 80%;
@@ -94,7 +94,6 @@
&.blame {
table {
border: none;
- box-shadow: none;
margin: 0;
}
tr {
@@ -108,19 +107,10 @@
border-right: none;
}
}
- img.avatar {
- border: 0 none;
- float: none;
- margin: 0;
- padding: 0;
- }
td.blame-commit {
- background: #f9f9f9;
- min-width: 350px;
-
- .commit-author-link {
- color: #888;
- }
+ padding: 0 10px;
+ min-width: 400px;
+ background: $gray-light;
}
td.line-numbers {
float: none;
@@ -133,12 +123,6 @@
}
td.lines {
padding: 0;
- code {
- font-family: $monospace_font;
- }
- pre {
- margin: 0;
- }
}
}
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 9209347f9bc..19827943385 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -1,6 +1,10 @@
.filter-item {
margin-right: 6px;
vertical-align: top;
+
+ &.reset-filters {
+ padding: 7px;
+ }
}
@media (min-width: $screen-sm-min) {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 1036219172e..d4a030f7f7a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -77,10 +77,6 @@ header {
}
}
- &.header-collapsed {
- padding: 0 16px;
- }
-
.side-nav-toggle {
position: absolute;
left: -10px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 62dc2cb3fdc..1ec08cdef23 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -44,7 +44,7 @@
}
&.active {
- background: #f9f9f9;
+ background: $gray-light;
a {
font-weight: 600;
}
@@ -85,3 +85,13 @@
#{'-webkit-' + $property}: $value;
#{$property}: $value;
}
+
+@mixin keyframes($animation-name) {
+ @-webkit-keyframes #{$animation-name} {
+ @content;
+ }
+
+ @keyframes #{$animation-name} {
+ @content;
+ }
+}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 9efbaf54e90..553768b2e68 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -138,7 +138,7 @@
}
li a {
- padding: 16px 10px 11px;
+ padding: 16px 15px 11px;
}
/* Small devices (phones, tablets, 768px and lower) */
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 015fe3debf9..3b7de4b57bb 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -1,6 +1,5 @@
.page-with-sidebar {
- padding-top: $header-height;
- padding-bottom: 25px;
+ padding: $header-height 0 25px;
transition: padding $sidebar-transition-duration;
&.page-sidebar-pinned {
@@ -15,6 +14,7 @@
bottom: 0;
left: 0;
height: 100%;
+ width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
@include box-shadow(2px 0 16px 0 $black-transparent);
@@ -128,10 +128,8 @@
.fa {
transition: transform .15s;
- }
- &.is-active {
- .fa {
+ .page-sidebar-pinned & {
transform: rotate(90deg);
}
}
@@ -152,14 +150,6 @@
}
}
-.page-sidebar-collapsed {
- padding-left: 0;
-
- .sidebar-wrapper {
- width: 0;
- }
-}
-
.page-sidebar-expanded {
.sidebar-wrapper {
width: $sidebar_width;
@@ -175,7 +165,7 @@
}
}
-header.header-pinned-nav {
+header.header-sidebar-pinned {
@media (min-width: $sidebar-breakpoint) {
padding-left: ($sidebar_width + $gl-padding);
diff --git a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
index 371c1bf17e1..915aa631ef8 100644
--- a/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
+++ b/app/assets/stylesheets/framework/tw_bootstrap_variables.scss
@@ -125,7 +125,7 @@ $panel-inner-border: $border-color;
//
//##
-$well-bg: #f9f9f9;
+$well-bg: $gray-light;
$well-border: #eee;
//== Code
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 06874a993fa..3f8433a0e7f 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -159,25 +159,18 @@
position: relative;
a.anchor {
- // Setting `display: none` would prevent the anchor being scrolled to, so
- // instead we set the height to 0 and it gets updated on hover.
- height: 0;
+ left: -16px;
+ position: absolute;
+ text-decoration: none;
+
+ &:after {
+ content: url('icon_anchor.svg');
+ visibility: hidden;
+ }
}
- &:hover > a.anchor {
- $size: 14px;
- position: absolute;
- right: 100%;
- top: 50%;
- margin-top: -11px;
- margin-right: 0;
- padding-right: 15px;
- display: inline-block;
- width: $size;
- height: $size;
- background-image: image-url("icon-link.png");
- background-size: contain;
- background-repeat: no-repeat;
+ &:hover > a.anchor:after {
+ visibility: visible;
}
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 5da390118c6..9f563a4de35 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -10,12 +10,78 @@ $sidebar-transition-duration: .15s;
$sidebar-breakpoint: 1024px;
/*
+ * Color schema
+ */
+$white-light: #fff;
+$white-normal: #ededed;
+$white-dark: #ececec;
+
+$gray-light: #fafafa;
+$gray-normal: #f5f5f5;
+$gray-dark: #ededed;
+$gray-darkest: #c9c9c9;
+
+$green-light: #38ae67;
+$green-normal: #2faa60;
+$green-dark: #2ca05b;
+
+$blue-light: #2ea8e5;
+$blue-normal: #2d9fd8;
+$blue-dark: #2897ce;
+
+$blue-medium-light: #3498cb;
+$blue-medium: #2f8ebf;
+$blue-medium-dark: #2d86b4;
+
+$orange-light: #fc8a51;
+$orange-normal: #e75e40;
+$orange-dark: #ce5237;
+
+$red-light: #e52c5a;
+$red-normal: #d22852;
+$red-dark: darken($red-normal, 5%);
+
+$black: #000;
+$black-transparent: rgba(0, 0, 0, 0.3);
+
+$border-white-light: #f1f2f4;
+$border-white-normal: #d6dae2;
+$border-white-dark: #c6cacf;
+
+$border-gray-light: #dcdcdc;
+$border-gray-normal: #d7d7d7;
+$border-gray-dark: #c6cacf;
+
+$border-green-light: #2faa60;
+$border-green-normal: #2ca05b;
+$border-green-dark: #279654;
+
+$border-blue-light: #2d9fd8;
+$border-blue-normal: #2897ce;
+$border-blue-dark: #258dc1;
+
+$border-orange-light: #fc6d26;
+$border-orange-normal: #ce5237;
+$border-orange-dark: #c14e35;
+
+$border-red-light: #d22852;
+$border-red-normal: #ca264f;
+$border-red-dark: darken($border-red-normal, 5%);
+
+$help-well-bg: $gray-light;
+$help-well-border: #e5e5e5;
+
+$warning-message-bg: #fbf2d9;
+$warning-message-color: #9e8e60;
+$warning-message-border: #f0e2bb;
+
+/*
* UI elements
*/
$border-color: #e5e5e5;
$focus-border-color: #3aabf0;
$table-border-color: #f0f0f0;
-$background-color: #fafafa;
+$background-color: $gray-light;
$dark-background-color: #f5f5f5;
$table-text-gray: #8f8f8f;
@@ -35,6 +101,7 @@ $gl-icon-color: $gl-placeholder-color;
$gl-grayish-blue: #7f8fa4;
$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
+$gl-gray-light: $gl-placeholder-color;
$gl-header-color: $gl-title-color;
/*
@@ -90,73 +157,6 @@ $btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-xs-side-margin: 5px;
-/*
- * Color schema
- */
-
-$white-light: #fff;
-$white-normal: #ededed;
-$white-dark: #ececec;
-
-$gray-light: #faf9f9;
-$gray-normal: #f5f5f5;
-$gray-dark: #ededed;
-$gray-darkest: #c9c9c9;
-
-$green-light: #38ae67;
-$green-normal: #2faa60;
-$green-dark: #2ca05b;
-
-$blue-light: #2ea8e5;
-$blue-normal: #2d9fd8;
-$blue-dark: #2897ce;
-
-$blue-medium-light: #3498cb;
-$blue-medium: #2f8ebf;
-$blue-medium-dark: #2d86b4;
-
-$orange-light: #fc8a51;
-$orange-normal: #e75e40;
-$orange-dark: #ce5237;
-
-$red-light: #e52c5a;
-$red-normal: #d22852;
-$red-dark: darken($red-normal, 5%);
-
-$black: #000;
-$black-transparent: rgba(0, 0, 0, 0.3);
-
-$border-white-light: #f1f2f4;
-$border-white-normal: #d6dae2;
-$border-white-dark: #c6cacf;
-
-$border-gray-light: #dcdcdc;
-$border-gray-normal: #d7d7d7;
-$border-gray-dark: #c6cacf;
-
-$border-green-light: #2faa60;
-$border-green-normal: #2ca05b;
-$border-green-dark: #279654;
-
-$border-blue-light: #2d9fd8;
-$border-blue-normal: #2897ce;
-$border-blue-dark: #258dc1;
-
-$border-orange-light: #fc6d26;
-$border-orange-normal: #ce5237;
-$border-orange-dark: #c14e35;
-
-$border-red-light: #d22852;
-$border-red-normal: #ca264f;
-$border-red-dark: darken($border-red-normal, 5%);
-
-$help-well-bg: #fafafa;
-$help-well-border: #e5e5e5;
-
-$warning-message-bg: #fbf2d9;
-$warning-message-color: #9e8e60;
-$warning-message-border: #f0e2bb;
-
/* tanuki logo colors */
$tanuki-red: #e24329;
$tanuki-orange: #fc6d26;
@@ -186,7 +186,7 @@ $line-removed-dark: #fac5cd;
$line-number-old: #f9d7dc;
$line-number-new: #ddfbe6;
$line-number-select: #fbf2da;
-$match-line: #fafafa;
+$match-line: $gray-light;
$table-border-gray: #f0f0f0;
$line-target-blue: #eaf3fc;
$line-select-yellow: #fcf8e7;
@@ -267,7 +267,7 @@ $zen-control-hover-color: #111;
$calendar-header-color: #b8b8b8;
$calendar-hover-bg: #ecf3fe;
$calendar-border-color: rgba(#000, .1);
-$calendar-unselectable-bg: #faf9f9;
+$calendar-unselectable-bg: $gray-light;
/*
* Personal Access Tokens
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 5faedfedd66..9282e0ae03b 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -93,11 +93,8 @@
}
.award-control {
- margin-right: 5px;
- margin-bottom: 5px;
- padding-left: 5px;
- padding-right: 5px;
- line-height: 20px;
+ margin: 3px 5px 3px 0;
+ padding: 6px 5px;
outline: 0;
&:hover,
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index cee198691c2..c879074c7fe 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -48,12 +48,6 @@
margin-bottom: 10px;
}
}
-
- .page-sidebar-collapsed {
- .scroll-controls {
- left: 70px;
- }
- }
}
.build-header {
@@ -108,13 +102,23 @@
}
.blocks-container {
- padding: $gl-padding;
+ padding: 0 $gl-padding;
}
.block {
width: 100%;
}
+ .js-build-variable {
+ color: $code-color;
+ }
+
+ .js-build-value {
+ padding: 2px 4px;
+ color: $black;
+ background-color: $white-light;
+ }
+
.build-sidebar-header {
padding: 0 $gl-padding $gl-padding;
@@ -123,6 +127,13 @@
}
}
+ .retry-link {
+ color: $gl-link-color;
+ &:hover {
+ text-decoration: underline;
+ }
+ }
+
.stage-item {
cursor: pointer;
@@ -132,7 +143,7 @@
}
.build-dropdown {
- padding: 0 $gl-padding;
+ padding: $gl-padding 0;
.dropdown-menu-toggle {
margin-top: 8px;
@@ -146,7 +157,6 @@
}
.builds-container {
- margin-top: $gl-padding;
background-color: $white-light;
border-top: 1px solid $border-color;
border-bottom: 1px solid $border-color;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 6a58b445afa..dc57a837155 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -18,8 +18,7 @@
}
.commit-row-title {
- line-height: 1;
- margin-bottom: 7px;
+ line-height: 1.35;
.notes_count {
float: right;
@@ -43,6 +42,7 @@
border: 1px solid $border-gray-dark;
border-radius: $border-radius-default;
margin-left: 5px;
+ line-height: 1;
&:hover {
background-color: darken($gray-light, 10%);
@@ -113,11 +113,13 @@
.commit-row-description {
font-size: 14px;
- border-left: 1px solid #eee;
+ border-left: 1px solid $btn-gray-hover;
padding: 10px 15px;
margin: 10px 0;
- background: #f9f9f9;
+ background: $gray-light;
display: none;
+ white-space: pre-line;
+ word-break: normal;
pre {
border: none;
@@ -134,7 +136,7 @@
.commit-row-info {
color: $gl-gray;
- line-height: 1;
+ line-height: 1.35;
a {
color: $gl-gray;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 0cd45fb90bf..1d00da1266c 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -60,7 +60,7 @@
pre {
border: none;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #777;
margin: 0 20px;
@@ -92,7 +92,7 @@
border: 1px solid #eee;
padding: 5px;
@include border-radius(5px);
- background: #f9f9f9;
+ background: $gray-light;
margin-left: 10px;
top: -6px;
img {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 46c4a11aa2e..63845e6b9ba 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -404,3 +404,18 @@
margin-bottom: $gl-padding;
}
}
+
+.issuable-list {
+ li {
+ .issue-check {
+ float: left;
+ padding-right: $gl-padding;
+ margin-bottom: 10px;
+ min-width: 15px;
+
+ .selected_issue {
+ vertical-align: text-top;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 3a3bb10feac..7a26b7ad497 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -7,17 +7,6 @@
margin-bottom: 2px;
}
- .issue-check {
- float: left;
- padding-right: 8px;
- margin-bottom: 10px;
- min-width: 15px;
-
- .selected_issue {
- vertical-align: text-top;
- }
- }
-
.issue-labels {
display: inline-block;
}
@@ -72,12 +61,12 @@ form.edit-issue {
}
&.closed {
- background: #f9f9f9;
+ background: $gray-light;
border-color: #e5e5e5;
}
&.merged {
- background: #f9f9f9;
+ background: $gray-light;
border-color: #e5e5e5;
}
}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 606459f82cd..38c7cd98e41 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -7,6 +7,7 @@
display: inline-block;
margin-right: 10px;
margin-bottom: 10px;
+ text-decoration: none;
}
&.suggest-colors-dropdown {
diff --git a/app/assets/stylesheets/pages/merge_conflicts.scss b/app/assets/stylesheets/pages/merge_conflicts.scss
index 1f499897c16..5ec660799e3 100644
--- a/app/assets/stylesheets/pages/merge_conflicts.scss
+++ b/app/assets/stylesheets/pages/merge_conflicts.scss
@@ -16,7 +16,7 @@ $colors: (
white_button_origin_chosen : #268ced,
white_header_not_chosen : #f0f0f0,
- white_line_not_chosen : #f9f9f9,
+ white_line_not_chosen : $gray-light,
dark_header_head_neutral : rgba(#3f3, .2),
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 7fdd79fa8b9..2a44b95de64 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -375,7 +375,7 @@
}
}
-.mr-version-switch {
+.mr-version-controls {
background: $background-color;
padding: $gl-btn-padding;
color: $gl-placeholder-color;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ee5d9de66d8..2d66ab25da6 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -490,6 +490,6 @@
.ci-status-icon-created {
svg {
- fill: $table-text-gray;
+ fill: $gray-darkest;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f2db373da52..3e6e50375f6 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -723,9 +723,15 @@ pre.light-well {
}
}
-.project-refs-form {
- .dropdown-menu {
- width: 300px;
+.project-refs-form .dropdown-menu, .dropdown-menu-projects {
+ width: 300px;
+
+ @media (min-width: $screen-sm-min) {
+ width: 500px;
+ }
+
+ a {
+ white-space: normal;
}
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 0340526a53a..68a5d1ae06c 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -99,7 +99,7 @@
pre {
border: none;
- background: #f9f9f9;
+ background: $gray-light;
border-radius: 0;
color: #777;
margin: 0 20px;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 538f211c65b..1778c069706 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -11,14 +11,8 @@
}
}
- .last-commit {
- max-width: 506px;
-
- .last-commit-content {
- @include str-truncated;
- width: calc(100% - 140px);
- margin-left: 3px;
- }
+ .add-to-tree {
+ vertical-align: top;
}
.tree-table {
@@ -32,6 +26,15 @@
line-height: 21px;
}
+ .last-commit {
+ @include str-truncated(60%);
+ }
+
+ .commit-history-link-spacer {
+ margin: 0 10px;
+ color: $table-border-color;
+ }
+
&:hover {
td {
background-color: $row-hover;
@@ -87,11 +90,17 @@
}
}
- .tree_commit {
- color: $gl-gray;
+ .tree-time-ago {
+ min-width: 135px;
+ color: $gl-gray-light;
+ }
+
+ .tree-commit {
+ max-width: 320px;
+ color: $gl-gray-light;
.tree-commit-link {
- color: $gl-gray;
+ color: $gl-gray-light;
&:hover {
text-decoration: underline;
diff --git a/app/assets/stylesheets/pages/xterm.scss b/app/assets/stylesheets/pages/xterm.scss
index 8d855ce99b0..c9846103762 100644
--- a/app/assets/stylesheets/pages/xterm.scss
+++ b/app/assets/stylesheets/pages/xterm.scss
@@ -20,6 +20,9 @@
$l-cyan: #8abeb7;
$l-white: $ci-text-color;
+ .term-bold {
+ font-weight: bold;
+ }
.term-italic {
font-style: italic;
}
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index a7af3cb8345..e06d12cfce1 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -7,19 +7,14 @@ module Ci
def create
@content = params[:content]
+ @error = Ci::GitlabCiYamlProcessor.validation_message(@content)
+ @status = @error.blank?
- if @content.blank?
- @status = false
- @error = "Please provide content of .gitlab-ci.yml"
- else
+ if @error.blank?
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
- @status = true
end
- rescue Ci::GitlabCiYamlProcessor::ValidationError, Psych::SyntaxError => e
- @error = e.message
- @status = false
rescue
@error = 'Undefined error'
@status = false
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index f40b62446e5..bb32bc502e6 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -3,21 +3,54 @@ module IssuableActions
included do
before_action :authorize_destroy_issuable!, only: :destroy
+ before_action :authorize_admin_issuable!, only: :bulk_update
end
def destroy
issuable.destroy
+ destroy_method = "destroy_#{issuable.class.name.underscore}".to_sym
+ TodoService.new.public_send(destroy_method, issuable, current_user)
name = issuable.class.name.titleize.downcase
flash[:notice] = "The #{name} was successfully deleted."
redirect_to polymorphic_path([@project.namespace.becomes(Namespace), @project, issuable.class])
end
+ def bulk_update
+ result = Issuable::BulkUpdateService.new(project, current_user, bulk_update_params).execute(resource_name)
+ quantity = result[:count]
+
+ render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
+ end
+
private
def authorize_destroy_issuable!
- unless current_user.can?(:"destroy_#{issuable.to_ability_name}", issuable)
+ unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable)
return access_denied!
end
end
+
+ def authorize_admin_issuable!
+ unless can?(current_user, :"admin_#{resource_name}", @project)
+ return access_denied!
+ end
+ end
+
+ def bulk_update_params
+ params.require(:update).permit(
+ :issuable_ids,
+ :assignee_id,
+ :milestone_id,
+ :state_event,
+ :subscription_event,
+ label_ids: [],
+ add_label_ids: [],
+ remove_label_ids: []
+ )
+ end
+
+ def resource_name
+ @resource_name ||= controller_name.singularize
+ end
end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index a69877edfd4..4cb3be41064 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -13,7 +13,7 @@ module ServiceParams
# `issue_events` and `merge_request_events` (singular!)
# See app/helpers/services_helper.rb for how we
# make those event names plural as special case.
- :issues_events, :merge_requests_events,
+ :issues_events, :confidential_issues_events, :merge_requests_events,
:notify_only_broken_builds, :notify_only_broken_pipelines,
:add_pusher, :send_from_committer_email, :disable_diffs,
:external_wiki_url, :notify, :color,
diff --git a/app/controllers/import/base_controller.rb b/app/controllers/import/base_controller.rb
index 7e8597a5eb3..256c41e6145 100644
--- a/app/controllers/import/base_controller.rb
+++ b/app/controllers/import/base_controller.rb
@@ -1,18 +1,17 @@
class Import::BaseController < ApplicationController
private
- def get_or_create_namespace
+ def find_or_create_namespace(name, owner)
+ return current_user.namespace if name == owner
+ return current_user.namespace unless current_user.can_create_group?
+
begin
- namespace = Group.create!(name: @target_namespace, path: @target_namespace, owner: current_user)
+ name = params[:target_namespace].presence || name
+ namespace = Group.create!(name: name, path: name, owner: current_user)
namespace.add_owner(current_user)
+ namespace
rescue ActiveRecord::RecordNotUnique, ActiveRecord::RecordInvalid
- namespace = Namespace.find_by_path_or_name(@target_namespace)
- unless current_user.can?(:create_projects, namespace)
- @already_been_taken = true
- return false
- end
+ Namespace.find_by_path_or_name(name)
end
-
- namespace
end
end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 944c73d139a..6ea54744da8 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -35,23 +35,20 @@ class Import::BitbucketController < Import::BaseController
end
def create
- @repo_id = params[:repo_id] || ""
- repo = client.project(@repo_id.gsub("___", "/"))
- @project_name = repo["slug"]
-
- repo_owner = repo["owner"]
- repo_owner = current_user.username if repo_owner == client.user["user"]["username"]
- @target_namespace = params[:new_namespace].presence || repo_owner
-
- namespace = get_or_create_namespace || (render and return)
+ @repo_id = params[:repo_id].to_s
+ repo = client.project(@repo_id.gsub('___', '/'))
+ @project_name = repo['slug']
+ @target_namespace = find_or_create_namespace(repo['owner'], client.user['user']['username'])
unless Gitlab::BitbucketImport::KeyAdder.new(repo, current_user, access_params).execute
- @access_denied = true
- render
- return
+ render 'deploy_key' and return
end
- @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+ if current_user.can?(:create_projects, @target_namespace)
+ @project = Gitlab::BitbucketImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ else
+ render 'unauthorized'
+ end
end
private
diff --git a/app/controllers/import/github_controller.rb b/app/controllers/import/github_controller.rb
index 9c1b0eb20f4..8c6bdd16383 100644
--- a/app/controllers/import/github_controller.rb
+++ b/app/controllers/import/github_controller.rb
@@ -41,14 +41,13 @@ class Import::GithubController < Import::BaseController
@repo_id = params[:repo_id].to_i
repo = client.repo(@repo_id)
@project_name = repo.name
+ @target_namespace = find_or_create_namespace(repo.owner.login, client.user.login)
- repo_owner = repo.owner.login
- repo_owner = current_user.username if repo_owner == client.user.login
- @target_namespace = params[:new_namespace].presence || repo_owner
-
- namespace = get_or_create_namespace || (render and return)
-
- @project = Gitlab::GithubImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+ if current_user.can?(:create_projects, @target_namespace)
+ @project = Gitlab::GithubImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ else
+ render 'unauthorized'
+ end
end
private
diff --git a/app/controllers/import/gitlab_controller.rb b/app/controllers/import/gitlab_controller.rb
index 08130ee8176..73837ffbe67 100644
--- a/app/controllers/import/gitlab_controller.rb
+++ b/app/controllers/import/gitlab_controller.rb
@@ -26,15 +26,14 @@ class Import::GitlabController < Import::BaseController
def create
@repo_id = params[:repo_id].to_i
repo = client.project(@repo_id)
- @project_name = repo["name"]
+ @project_name = repo['name']
+ @target_namespace = find_or_create_namespace(repo['namespace']['path'], client.user['username'])
- repo_owner = repo["namespace"]["path"]
- repo_owner = current_user.username if repo_owner == client.user["username"]
- @target_namespace = params[:new_namespace].presence || repo_owner
-
- namespace = get_or_create_namespace || (render and return)
-
- @project = Gitlab::GitlabImport::ProjectCreator.new(repo, namespace, current_user, access_params).execute
+ if current_user.can?(:create_projects, @target_namespace)
+ @project = Gitlab::GitlabImport::ProjectCreator.new(repo, @target_namespace, current_user, access_params).execute
+ else
+ render 'unauthorized'
+ end
end
private
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index 12195c3cbb8..77934ff9962 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -78,8 +78,8 @@ class Projects::BuildsController < Projects::ApplicationController
end
def raw
- if @build.has_trace?
- send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline'
+ if @build.has_trace_file?
+ send_file @build.trace_file_path, type: 'text/plain; charset=utf-8', disposition: 'inline'
else
render_404
end
diff --git a/app/controllers/projects/git_http_client_controller.rb b/app/controllers/projects/git_http_client_controller.rb
index a5b4031c30f..f5ce63fdfed 100644
--- a/app/controllers/projects/git_http_client_controller.rb
+++ b/app/controllers/projects/git_http_client_controller.rb
@@ -117,4 +117,8 @@ class Projects::GitHttpClientController < Projects::ApplicationController
def ci?
@ci.present?
end
+
+ def verify_workhorse_api!
+ Gitlab::Workhorse.verify_api_request!(request.headers)
+ end
end
diff --git a/app/controllers/projects/git_http_controller.rb b/app/controllers/projects/git_http_controller.rb
index b4373ef89ef..9805705c4e3 100644
--- a/app/controllers/projects/git_http_controller.rb
+++ b/app/controllers/projects/git_http_controller.rb
@@ -1,6 +1,8 @@
# This file should be identical in GitLab Community Edition and Enterprise Edition
class Projects::GitHttpController < Projects::GitHttpClientController
+ before_action :verify_workhorse_api!
+
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
@@ -56,6 +58,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end
def render_ok
+ set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, user)
end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index b5624046387..0ae8ff98009 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -59,6 +59,7 @@ class Projects::HooksController < Projects::ApplicationController
:pipeline_events,
:enable_ssl_verification,
:issues_events,
+ :confidential_issues_events,
:merge_requests_events,
:note_events,
:push_events,
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 72d2d361878..de02e28e384 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -20,9 +20,6 @@ class Projects::IssuesController < Projects::ApplicationController
# Allow modify issue
before_action :authorize_update_issue!, only: [:edit, :update]
- # Allow issues bulk update
- before_action :authorize_admin_issues!, only: [:bulk_update]
-
respond_to :html
def index
@@ -168,16 +165,6 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
- def bulk_update
- result = Issues::BulkUpdateService.new(project, current_user, bulk_update_params).execute
-
- respond_to do |format|
- format.json do
- render json: { notice: "#{result[:count]} issues updated" }
- end
- end
- end
-
protected
def issue
@@ -237,17 +224,4 @@ class Projects::IssuesController < Projects::ApplicationController
:milestone_id, :due_date, :state_event, :task_num, :lock_version, label_ids: []
)
end
-
- def bulk_update_params
- params.require(:update).permit(
- :issues_ids,
- :assignee_id,
- :milestone_id,
- :state_event,
- :subscription_event,
- label_ids: [],
- add_label_ids: [],
- remove_label_ids: []
- )
- end
end
diff --git a/app/controllers/projects/lfs_storage_controller.rb b/app/controllers/projects/lfs_storage_controller.rb
index 69066cb40e6..9005b104e90 100644
--- a/app/controllers/projects/lfs_storage_controller.rb
+++ b/app/controllers/projects/lfs_storage_controller.rb
@@ -3,6 +3,7 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
before_action :require_lfs_enabled!
before_action :lfs_check_access!
+ before_action :verify_workhorse_api!, only: [:upload_authorize]
def download
lfs_object = LfsObject.find_by_oid(oid)
@@ -15,14 +16,8 @@ class Projects::LfsStorageController < Projects::GitHttpClientController
end
def upload_authorize
- render(
- json: {
- StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
- LfsOid: oid,
- LfsSize: size,
- },
- content_type: 'application/json; charset=utf-8'
- )
+ set_workhorse_internal_api_content_type
+ render json: Gitlab::Workhorse.lfs_upload_ok(oid, size)
end
def upload_finalize
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 4f9ca0097a1..8895cb955bd 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -90,16 +90,27 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.merge_request_diff
end
+ @merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
+ @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
+
+ if params[:start_sha].present?
+ @start_sha = params[:start_sha]
+ @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha }
+
+ unless @start_version
+ render_404
+ end
+ end
+
respond_to do |format|
format.html { define_discussion_vars }
format.json do
- unless @merge_request_diff.latest?
- # Disable comments if browsing older version of the diff
- @diff_notes_disabled = true
+ if @start_sha
+ compared_diff_version
+ else
+ original_diff_version
end
- @diffs = @merge_request_diff.diffs(diff_options)
-
render json: { html: view_to_html_string("projects/merge_requests/show/_diffs") }
end
end
@@ -529,4 +540,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
@merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
end
+
+ def compared_diff_version
+ @diff_notes_disabled = true
+ @diffs = @merge_request_diff.compare_with(@start_sha).diffs(diff_options)
+ end
+
+ def original_diff_version
+ @diff_notes_disabled = !@merge_request_diff.latest?
+ @diffs = @merge_request_diff.diffs(diff_options)
+ end
end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index b0c72cfe4b4..371cc3787fb 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -7,11 +7,10 @@ class Projects::PipelinesController < Projects::ApplicationController
def index
@scope = params[:scope]
- all_pipelines = project.pipelines
- @pipelines_count = all_pipelines.count
- @running_or_pending_count = all_pipelines.running_or_pending.count
- @pipelines = PipelinesFinder.new(project).execute(all_pipelines, @scope)
- @pipelines = @pipelines.order(id: :desc).page(params[:page]).per(30)
+ @pipelines = PipelinesFinder.new(project).execute(scope: @scope).page(params[:page]).per(30)
+
+ @running_or_pending_count = PipelinesFinder.new(project).execute(scope: 'running').count
+ @pipelines_count = PipelinesFinder.new(project).execute.count
end
def new
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 6a227d85f6f..97e6e9471e0 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -20,9 +20,8 @@ class Projects::ServicesController < Projects::ApplicationController
def update
if @service.update_attributes(service_params[:service])
redirect_to(
- edit_namespace_project_service_path(@project.namespace, @project,
- @service.to_param, notice:
- 'Successfully updated.')
+ edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
+ notice: 'Successfully updated.'
)
else
render 'edit'
diff --git a/app/finders/pipelines_finder.rb b/app/finders/pipelines_finder.rb
index 641fbf838f1..32aea75486d 100644
--- a/app/finders/pipelines_finder.rb
+++ b/app/finders/pipelines_finder.rb
@@ -1,30 +1,34 @@
class PipelinesFinder
- attr_reader :project
+ attr_reader :project, :pipelines
def initialize(project)
@project = project
+ @pipelines = project.pipelines
end
- def execute(pipelines, scope)
- case scope
- when 'running'
- pipelines.running_or_pending
- when 'branches'
- from_ids(pipelines, ids_for_ref(pipelines, branches))
- when 'tags'
- from_ids(pipelines, ids_for_ref(pipelines, tags))
- else
- pipelines
- end
+ def execute(scope: nil)
+ scoped_pipelines =
+ case scope
+ when 'running'
+ pipelines.running_or_pending
+ when 'branches'
+ from_ids(ids_for_ref(branches))
+ when 'tags'
+ from_ids(ids_for_ref(tags))
+ else
+ pipelines
+ end
+
+ scoped_pipelines.order(id: :desc)
end
private
- def ids_for_ref(pipelines, refs)
+ def ids_for_ref(refs)
pipelines.where(ref: refs).group(:ref).select('max(id)')
end
- def from_ids(pipelines, ids)
+ def from_ids(ids)
pipelines.unscoped.where(id: ids)
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index aa8acbe7567..df41473543b 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -14,7 +14,8 @@ module AvatarsHelper
avatar_icon(options[:user] || options[:user_email], avatar_size),
class: "avatar has-tooltip hidden-xs s#{avatar_size}",
alt: "#{user_name}'s avatar",
- title: user_name
+ title: user_name,
+ data: { container: 'body' }
)
if options[:user]
diff --git a/app/helpers/git_helper.rb b/app/helpers/git_helper.rb
index 09684955233..8ab394384f3 100644
--- a/app/helpers/git_helper.rb
+++ b/app/helpers/git_helper.rb
@@ -2,4 +2,8 @@ module GitHelper
def strip_gpg_signature(text)
text.gsub(/-----BEGIN PGP SIGNATURE-----(.*)-----END PGP SIGNATURE-----/m, "")
end
+
+ def short_sha(text)
+ Commit.truncate_sha(text)
+ end
end
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index 109bc1a02d1..021d2b14718 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -1,4 +1,9 @@
module ImportHelper
+ def import_project_target(owner, name)
+ namespace = current_user.can_create_group? ? owner : current_user.namespace_path
+ "#{namespace}/#{name}"
+ end
+
def github_project_link(path_with_namespace)
link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
end
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index a9e175c3f5c..8abe7865fed 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -100,4 +100,14 @@ module MergeRequestsHelper
def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
+
+ def merge_request_version_path(project, merge_request, merge_request_diff, start_sha = nil)
+ diffs_namespace_project_merge_request_path(
+ project.namespace, project, merge_request,
+ diff_id: merge_request_diff.id, start_sha: start_sha)
+ end
+
+ def version_index(merge_request_diff)
+ @merge_request_diffs.size - @merge_request_diffs.index(merge_request_diff)
+ end
end
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 2b0ff6c0d00..df87fac132d 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -1,21 +1,7 @@
module NavHelper
- def nav_menu_collapsed?
- cookies[:collapsed_nav] == 'true'
- end
-
- def nav_sidebar_class
- if nav_menu_collapsed?
- "sidebar-collapsed"
- else
- "sidebar-expanded"
- end
- end
-
def page_sidebar_class
if pinned_nav?
"page-sidebar-expanded page-sidebar-pinned"
- else
- "page-sidebar-collapsed"
end
end
@@ -26,7 +12,6 @@ module NavHelper
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"
@@ -43,9 +28,7 @@ module NavHelper
class_name << " with-horizontal-nav" if defined?(nav) && nav
if pinned_nav?
- class_name << " header-expanded header-pinned-nav"
- else
- class_name << " header-collapsed"
+ class_name << " header-sidebar-expanded header-sidebar-pinned"
end
class_name
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d6efa603223..16a8e52a4ca 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -129,6 +129,19 @@ module ProjectsHelper
current_user.recent_push(project_ids)
end
+ def project_feature_access_select(field)
+ # Don't show option "everyone with access" if project is private
+ options = project_feature_options
+
+ if @project.private?
+ options.delete('Everyone with access')
+ highest_available_option = options.values.max if @project.project_feature.send(field) == ProjectFeature::ENABLED
+ end
+
+ options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
+ content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe
+ end
+
private
def get_project_nav_tabs(project, current_user)
@@ -422,15 +435,4 @@ module ProjectsHelper
'Everyone with access' => ProjectFeature::ENABLED
}
end
-
- def project_feature_access_select(field)
- # Don't show option "everyone with access" if project is private
- options = project_feature_options
- level = @project.project_feature.public_send(field)
-
- options.delete('Everyone with access') if @project.private? && level != ProjectFeature::ENABLED
-
- options = options_for_select(options, selected: @project.project_feature.public_send(field) || ProjectFeature::ENABLED)
- content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control").html_safe
- end
end
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 4549c2e5bb6..e523c46e879 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -7,8 +7,10 @@ module SearchHelper
projects_autocomplete(term)
].flatten
+ search_pattern = Regexp.new(Regexp.escape(term), "i")
+
generic_results = project_autocomplete + default_autocomplete + help_autocomplete
- generic_results.select! { |result| result[:label] =~ Regexp.new(term, "i") }
+ generic_results.select! { |result| result[:label] =~ search_pattern }
[
resources_results,
diff --git a/app/helpers/services_helper.rb b/app/helpers/services_helper.rb
index 2dd0bf5d71e..3d4abf76419 100644
--- a/app/helpers/services_helper.rb
+++ b/app/helpers/services_helper.rb
@@ -8,7 +8,9 @@ module ServicesHelper
when "note"
"Event will be triggered when someone adds a comment"
when "issue"
- "Event will be triggered when an issue is created/updated/merged"
+ "Event will be triggered when an issue is created/updated/closed"
+ when "confidential_issue"
+ "Event will be triggered when a confidential issue is created/updated/closed"
when "merge_request"
"Event will be triggered when a merge request is created/updated/merged"
when "build"
@@ -19,7 +21,7 @@ module ServicesHelper
end
def service_event_field_name(event)
- event = event.pluralize if %w[merge_request issue].include?(event)
+ event = event.pluralize if %w[merge_request issue confidential_issue].include?(event)
"#{event}_events"
end
end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
new file mode 100644
index 00000000000..d440edc55ba
--- /dev/null
+++ b/app/helpers/sidekiq_helper.rb
@@ -0,0 +1,19 @@
+module SidekiqHelper
+ SIDEKIQ_PS_REGEXP = /\A
+ (?<pid>\d+)\s+
+ (?<cpu>[\d\.,]+)\s+
+ (?<mem>[\d\.,]+)\s+
+ (?<state>[DRSTWXZNLsl\+<]+)\s+
+ (?<start>.+)\s+
+ (?<command>sidekiq.*\])\s+
+ \z/x
+
+ def parse_sidekiq_ps(line)
+ match = line.match(SIDEKIQ_PS_REGEXP)
+ if match
+ match[1..6]
+ else
+ %w[? ? ? ? ? ?]
+ end
+ end
+end
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index d887cdadc34..88f374be1e5 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -34,4 +34,8 @@ module WorkhorseHelper
headers.store(*Gitlab::Workhorse.send_artifacts_entry(build, entry))
head :ok
end
+
+ def set_workhorse_internal_api_content_type
+ headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 23c8de6f650..61052437318 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -208,22 +208,31 @@ module Ci
end
end
+ def has_trace_file?
+ File.exist?(path_to_trace) || has_old_trace_file?
+ end
+
def has_trace?
raw_trace.present?
end
def raw_trace
- if File.file?(path_to_trace)
- File.read(path_to_trace)
- elsif project.ci_id && File.file?(old_path_to_trace)
- # Temporary fix for build trace data integrity
- File.read(old_path_to_trace)
+ if File.exist?(trace_file_path)
+ File.read(trace_file_path)
else
# backward compatibility
read_attribute :trace
end
end
+ ##
+ # Deprecated
+ #
+ # This is a hotfix for CI build data integrity, see #4246
+ def has_old_trace_file?
+ project.ci_id && File.exist?(old_path_to_trace)
+ end
+
def trace
trace = raw_trace
if project && trace.present? && project.runners_token.present?
@@ -262,6 +271,14 @@ module Ci
end
end
+ def trace_file_path
+ if has_old_trace_file?
+ old_path_to_trace
+ else
+ path_to_trace
+ end
+ end
+
def dir_to_trace
File.join(
Settings.gitlab_ci.builds_path,
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index bd1737c7587..0b1df9f4294 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -257,8 +257,17 @@ module Ci
]
end
+ def queued_duration
+ return unless started_at
+
+ seconds = (started_at - created_at).to_i
+ seconds unless seconds.zero?
+ end
+
def update_duration
- self.duration = calculate_duration
+ return unless started_at
+
+ self.duration = Gitlab::Ci::PipelineDuration.from_pipeline(self)
end
def execute_hooks
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 49f05f881a2..ed5d4b13b7e 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,7 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
- LAST_CONTACT_TIME = 5.minutes.ago
+ LAST_CONTACT_TIME = 2.hours.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 4442cefc7e9..559b3075905 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -13,6 +13,11 @@ class DiffNote < Note
validate :positions_complete
validate :verify_supported
+ # Keep this scope in sync with the logic in `#resolvable?`
+ scope :resolvable, -> { user.where(noteable_type: 'MergeRequest') }
+ scope :resolved, -> { resolvable.where.not(resolved_at: nil) }
+ scope :unresolved, -> { resolvable.where(resolved_at: nil) }
+
after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id
@@ -25,6 +30,16 @@ class DiffNote < Note
def build_discussion_id(noteable_type, noteable_id, position)
[super(noteable_type, noteable_id), *position.key].join("-")
end
+
+ # This method must be kept in sync with `#resolve!`
+ def resolve!(current_user)
+ unresolved.update_all(resolved_at: Time.now, resolved_by_id: current_user.id)
+ end
+
+ # This method must be kept in sync with `#unresolve!`
+ def unresolve!
+ resolved.update_all(resolved_at: nil, resolved_by_id: nil)
+ end
end
def new_diff_note?
@@ -73,6 +88,7 @@ class DiffNote < Note
self.position.diff_refs == diff_refs
end
+ # If you update this method remember to also update the scope `resolvable`
def resolvable?
!system? && for_merge_request?
end
@@ -83,6 +99,7 @@ class DiffNote < Note
self.resolved_at.present?
end
+ # If you update this method remember to also update `.resolve!`
def resolve!(current_user)
return unless resolvable?
return if resolved?
@@ -92,6 +109,7 @@ class DiffNote < Note
save!
end
+ # If you update this method remember to also update `.unresolve!`
def unresolve!
return unless resolvable?
return unless resolved?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 9676bc03470..de06c13481a 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -1,7 +1,7 @@
class Discussion
NUMBER_OF_TRUNCATED_DIFF_LINES = 16
- attr_reader :first_note, :last_note, :notes
+ attr_reader :notes
delegate :created_at,
:project,
@@ -36,8 +36,6 @@ class Discussion
end
def initialize(notes)
- @first_note = notes.first
- @last_note = notes.last
@notes = notes
end
@@ -70,17 +68,25 @@ class Discussion
end
def resolvable?
- return @resolvable if defined?(@resolvable)
+ return @resolvable if @resolvable.present?
@resolvable = diff_discussion? && notes.any?(&:resolvable?)
end
def resolved?
- return @resolved if defined?(@resolved)
+ return @resolved if @resolved.present?
@resolved = resolvable? && notes.none?(&:to_be_resolved?)
end
+ def first_note
+ @first_note ||= @notes.first
+ end
+
+ def last_note
+ @last_note ||= @notes.last
+ end
+
def resolved_notes
notes.select(&:resolved?)
end
@@ -100,17 +106,13 @@ class Discussion
def resolve!(current_user)
return unless resolvable?
- notes.each do |note|
- note.resolve!(current_user) if note.resolvable?
- end
+ update { |notes| notes.resolve!(current_user) }
end
def unresolve!
return unless resolvable?
- notes.each do |note|
- note.unresolve! if note.resolvable?
- end
+ update { |notes| notes.unresolve! }
end
def for_target?(target)
@@ -118,7 +120,7 @@ class Discussion
end
def active?
- return @active if defined?(@active)
+ return @active if @active.present?
@active = first_note.active?
end
@@ -174,4 +176,17 @@ class Discussion
prev_lines
end
+
+ private
+
+ def update
+ notes_relation = DiffNote.where(id: notes.map(&:id)).fresh
+ yield(notes_relation)
+
+ # Set the notes array to the updated notes
+ @notes = notes_relation.to_a
+
+ # Reset the memoized values
+ @last_resolved_note = @resolvable = @resolved = @first_note = @last_note = nil
+ end
end
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index 836a75b0608..c631e7a7df5 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -2,6 +2,7 @@ class ProjectHook < WebHook
belongs_to :project
scope :issue_hooks, -> { where(issues_events: true) }
+ scope :confidential_issue_hooks, -> { where(confidential_issues_events: true) }
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index f365dee3141..595602e80fe 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -4,6 +4,7 @@ class WebHook < ActiveRecord::Base
default_value_for :push_events, true
default_value_for :issues_events, false
+ default_value_for :confidential_issues_events, false
default_value_for :note_events, false
default_value_for :merge_requests_events, false
default_value_for :tag_push_events, false
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 445179a4487..18c583add88 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -152,6 +152,10 @@ class MergeRequestDiff < ActiveRecord::Base
self == merge_request.merge_request_diff
end
+ def compare_with(sha)
+ CompareService.new.execute(project, head_commit_sha, project, sha)
+ end
+
private
def dump_commits(commits)
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index d7c986c1a91..afebd3b6a12 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -39,7 +39,7 @@ class HipchatService < Service
end
def supported_events
- %w(push issue merge_request note tag_push build)
+ %w(push issue confidential_issue merge_request note tag_push build)
end
def execute(data)
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index abbc780dc1a..e1b937817f4 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -1,6 +1,6 @@
class SlackService < Service
prop_accessor :webhook, :username, :channel
- boolean_accessor :notify_only_broken_builds
+ boolean_accessor :notify_only_broken_builds, :notify_only_broken_pipelines
validates :webhook, presence: true, url: true, if: :activated?
def initialize_properties
@@ -10,6 +10,7 @@ class SlackService < Service
if properties.nil?
self.properties = {}
self.notify_only_broken_builds = true
+ self.notify_only_broken_pipelines = true
end
end
@@ -38,13 +39,15 @@ class SlackService < Service
{ type: 'text', name: 'username', placeholder: 'username' },
{ type: 'text', name: 'channel', placeholder: "#general" },
{ type: 'checkbox', name: 'notify_only_broken_builds' },
+ { type: 'checkbox', name: 'notify_only_broken_pipelines' },
]
default_fields + build_event_channels
end
def supported_events
- %w(push issue merge_request note tag_push build wiki_page)
+ %w[push issue confidential_issue merge_request note tag_push
+ build pipeline wiki_page]
end
def execute(data)
@@ -62,32 +65,22 @@ class SlackService < Service
# 'close' action. Ignore update events for now to prevent duplicate
# messages from arriving.
- message = \
- case object_kind
- when "push", "tag_push"
- PushMessage.new(data)
- when "issue"
- IssueMessage.new(data) unless is_update?(data)
- when "merge_request"
- MergeMessage.new(data) unless is_update?(data)
- when "note"
- NoteMessage.new(data)
- when "build"
- BuildMessage.new(data) if should_build_be_notified?(data)
- when "wiki_page"
- WikiPageMessage.new(data)
- end
-
- opt = {}
-
- event_channel = get_channel_field(object_kind) || channel
-
- opt[:channel] = event_channel if event_channel
- opt[:username] = username if username
+ message = get_message(object_kind, data)
if message
+ opt = {}
+
+ event_channel = get_channel_field(object_kind) || channel
+
+ opt[:channel] = event_channel if event_channel
+ opt[:username] = username if username
+
notifier = Slack::Notifier.new(webhook, opt)
notifier.ping(message.pretext, attachments: message.attachments, fallback: message.fallback)
+
+ true
+ else
+ false
end
end
@@ -105,6 +98,25 @@ class SlackService < Service
private
+ def get_message(object_kind, data)
+ case object_kind
+ when "push", "tag_push"
+ PushMessage.new(data)
+ when "issue"
+ IssueMessage.new(data) unless is_update?(data)
+ when "merge_request"
+ MergeMessage.new(data) unless is_update?(data)
+ when "note"
+ NoteMessage.new(data)
+ when "build"
+ BuildMessage.new(data) if should_build_be_notified?(data)
+ when "pipeline"
+ PipelineMessage.new(data) if should_pipeline_be_notified?(data)
+ when "wiki_page"
+ WikiPageMessage.new(data)
+ end
+ end
+
def get_channel_field(event)
field_name = event_channel_name(event)
self.public_send(field_name)
@@ -142,6 +154,17 @@ class SlackService < Service
false
end
end
+
+ def should_pipeline_be_notified?(data)
+ case data[:object_attributes][:status]
+ when 'success'
+ !notify_only_broken_pipelines?
+ when 'failed'
+ true
+ else
+ false
+ end
+ end
end
require "slack_service/issue_message"
@@ -149,4 +172,5 @@ require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
require "slack_service/build_message"
+require "slack_service/pipeline_message"
require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/slack_service/build_message.rb b/app/models/project_services/slack_service/build_message.rb
index 69c21b3fc38..0fca4267bad 100644
--- a/app/models/project_services/slack_service/build_message.rb
+++ b/app/models/project_services/slack_service/build_message.rb
@@ -9,7 +9,7 @@ class SlackService
attr_reader :user_name
attr_reader :duration
- def initialize(params, commit = true)
+ def initialize(params)
@sha = params[:sha]
@ref_type = params[:tag] ? 'tag' : 'branch'
@ref = params[:ref]
@@ -36,7 +36,7 @@ class SlackService
def message
"#{project_link}: Commit #{commit_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
- end
+ end
def format(string)
Slack::Notifier::LinkFormatter.format(string)
diff --git a/app/models/project_services/slack_service/pipeline_message.rb b/app/models/project_services/slack_service/pipeline_message.rb
new file mode 100644
index 00000000000..f06b3562965
--- /dev/null
+++ b/app/models/project_services/slack_service/pipeline_message.rb
@@ -0,0 +1,79 @@
+class SlackService
+ class PipelineMessage < BaseMessage
+ attr_reader :sha, :ref_type, :ref, :status, :project_name, :project_url,
+ :user_name, :duration, :pipeline_id
+
+ def initialize(data)
+ pipeline_attributes = data[:object_attributes]
+ @sha = pipeline_attributes[:sha]
+ @ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
+ @ref = pipeline_attributes[:ref]
+ @status = pipeline_attributes[:status]
+ @duration = pipeline_attributes[:duration]
+ @pipeline_id = pipeline_attributes[:id]
+
+ @project_name = data[:project][:path_with_namespace]
+ @project_url = data[:project][:web_url]
+ @user_name = data[:commit] && data[:commit][:author_name]
+ end
+
+ def pretext
+ ''
+ end
+
+ def fallback
+ format(message)
+ end
+
+ def attachments
+ [{ text: format(message), color: attachment_color }]
+ end
+
+ private
+
+ def message
+ "#{project_link}: Pipeline #{pipeline_link} of #{branch_link} #{ref_type} by #{user_name} #{humanized_status} in #{duration} #{'second'.pluralize(duration)}"
+ end
+
+ def format(string)
+ Slack::Notifier::LinkFormatter.format(string)
+ end
+
+ def humanized_status
+ case status
+ when 'success'
+ 'passed'
+ else
+ status
+ end
+ end
+
+ def attachment_color
+ if status == 'success'
+ 'good'
+ else
+ 'danger'
+ end
+ end
+
+ def branch_url
+ "#{project_url}/commits/#{ref}"
+ end
+
+ def branch_link
+ "[#{ref}](#{branch_url})"
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def pipeline_url
+ "#{project_url}/pipelines/#{pipeline_id}"
+ end
+
+ def pipeline_link
+ "[#{Commit.truncate_sha(sha)}](#{pipeline_url})"
+ end
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index f891e8374d2..7b7090b8a73 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -149,7 +149,7 @@ class Repository
return false unless target
GitHooksService.new.execute(user, path_to_repo, oldrev, target, ref) do
- rugged.branches.create(branch_name, target)
+ update_ref!(ref, target, oldrev)
end
after_create_branch
@@ -181,7 +181,7 @@ class Repository
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
GitHooksService.new.execute(user, path_to_repo, oldrev, newrev, ref) do
- rugged.branches.delete(branch_name)
+ update_ref!(ref, newrev, oldrev)
end
after_remove_branch
@@ -215,6 +215,21 @@ class Repository
rugged.references.exist?(ref)
end
+ def update_ref!(name, newrev, oldrev)
+ # We use 'git update-ref' because libgit2/rugged currently does not
+ # offer 'compare and swap' ref updates. Without compare-and-swap we can
+ # (and have!) accidentally reset the ref to an earlier state, clobbering
+ # commits. See also https://github.com/libgit2/libgit2/issues/1534.
+ command = %w[git update-ref --stdin -z]
+ _, status = Gitlab::Popen.popen(command, path_to_repo) do |stdin|
+ stdin.write("update #{name}\x00#{newrev}\x00#{oldrev}\x00")
+ end
+
+ return if status.zero?
+
+ raise CommitError.new("Could not update branch #{name.sub('refs/heads/', '')}. Please refresh and try again.")
+ end
+
# Makes sure a commit is kept around when Git garbage collection runs.
# Git GC will delete commits from the repository that are no longer in any
# branches or tags, but we want to keep some of these commits around, for
@@ -742,7 +757,7 @@ class Repository
end
def commit_dir(user, path, message, branch)
- commit_with_hooks(user, branch) do |ref|
+ update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
options = {}
options[:committer] = committer
@@ -759,7 +774,7 @@ class Repository
end
def commit_file(user, path, content, message, branch, update)
- commit_with_hooks(user, branch) do |ref|
+ update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
options = {}
options[:committer] = committer
@@ -781,7 +796,7 @@ class Repository
end
def update_file(user, path, content, branch:, previous_path:, message:)
- commit_with_hooks(user, branch) do |ref|
+ update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
options = {}
options[:committer] = committer
@@ -808,7 +823,7 @@ class Repository
end
def remove_file(user, path, message, branch)
- commit_with_hooks(user, branch) do |ref|
+ update_branch_with_hooks(user, branch) do |ref|
committer = user_to_committer(user)
options = {}
options[:committer] = committer
@@ -856,7 +871,7 @@ class Repository
merge_index = rugged.merge_commits(our_commit, their_commit)
return false if merge_index.conflicts?
- commit_with_hooks(user, merge_request.target_branch) do
+ update_branch_with_hooks(user, merge_request.target_branch) do
actual_options = options.merge(
parents: [our_commit, their_commit],
tree: merge_index.write_tree(rugged),
@@ -874,7 +889,7 @@ class Repository
return false unless revert_tree_id
- commit_with_hooks(user, base_branch) do
+ update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.revert_message,
@@ -891,7 +906,7 @@ class Repository
return false unless cherry_pick_tree_id
- commit_with_hooks(user, base_branch) do
+ update_branch_with_hooks(user, base_branch) do
committer = user_to_committer(user)
source_sha = Rugged::Commit.create(rugged,
message: commit.message,
@@ -907,7 +922,7 @@ class Repository
end
def resolve_conflicts(user, branch, params)
- commit_with_hooks(user, branch) do
+ update_branch_with_hooks(user, branch) do
committer = user_to_committer(user)
Rugged::Commit.create(rugged, params.merge(author: committer, committer: committer))
@@ -1011,18 +1026,13 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo)
end
- def commit_with_hooks(current_user, branch)
+ def update_branch_with_hooks(current_user, branch)
update_autocrlf_option
- oldrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch
target_branch = find_branch(branch)
was_empty = empty?
- if !was_empty && target_branch
- oldrev = target_branch.target.id
- end
-
# Make commit
newrev = yield(ref)
@@ -1030,24 +1040,19 @@ class Repository
raise CommitError.new('Failed to create commit')
end
+ if rugged.lookup(newrev).parent_ids.empty? || target_branch.nil?
+ oldrev = Gitlab::Git::BLANK_SHA
+ else
+ oldrev = rugged.merge_base(newrev, target_branch.target.sha)
+ end
+
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
+ update_ref!(ref, newrev, oldrev)
+
if was_empty || !target_branch
- # Create branch
- rugged.references.create(ref, newrev)
-
# If repo was empty expire cache
after_create if was_empty
after_create_branch
- else
- # Update head
- current_head = find_branch(branch).target.id
-
- # Make sure target branch was not changed during pre-receive hook
- if current_head == oldrev
- rugged.references.update(ref, newrev)
- else
- raise CommitError.new('Commit was rejected because branch received new push')
- end
end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 09b4717a523..198e7247838 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -7,6 +7,7 @@ class Service < ActiveRecord::Base
default_value_for :active, false
default_value_for :push_events, true
default_value_for :issues_events, true
+ default_value_for :confidential_issues_events, true
default_value_for :merge_requests_events, true
default_value_for :tag_push_events, true
default_value_for :note_events, true
@@ -33,6 +34,7 @@ class Service < ActiveRecord::Base
scope :push_hooks, -> { where(push_events: true, active: true) }
scope :tag_push_hooks, -> { where(tag_push_events: true, active: true) }
scope :issue_hooks, -> { where(issues_events: true, active: true) }
+ scope :confidential_issue_hooks, -> { where(confidential_issues_events: true, active: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
@@ -100,7 +102,7 @@ class Service < ActiveRecord::Base
end
def supported_events
- %w(push tag_push issue merge_request wiki_page)
+ %w(push tag_push issue confidential_issue merge_request wiki_page)
end
def execute(data)
diff --git a/app/services/issuable/bulk_update_service.rb b/app/services/issuable/bulk_update_service.rb
new file mode 100644
index 00000000000..60891cbb255
--- /dev/null
+++ b/app/services/issuable/bulk_update_service.rb
@@ -0,0 +1,26 @@
+module Issuable
+ class BulkUpdateService < IssuableBaseService
+ def execute(type)
+ model_class = type.classify.constantize
+ update_class = type.classify.pluralize.constantize::UpdateService
+
+ ids = params.delete(:issuable_ids).split(",")
+ items = model_class.where(id: ids)
+
+ %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
+ params.delete(key) unless params[key].present?
+ end
+
+ items.each do |issuable|
+ next unless can?(current_user, :"update_#{type}", issuable)
+
+ update_class.new(issuable.project, current_user, params).execute(issuable)
+ end
+
+ {
+ count: items.count,
+ success: !items.count.zero?
+ }
+ end
+ end
+end
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 089b0f527e2..9ea3ce084ba 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -14,9 +14,10 @@ module Issues
end
def execute_hooks(issue, action = 'open')
- issue_data = hook_data(issue, action)
- issue.project.execute_hooks(issue_data, :issue_hooks)
- issue.project.execute_services(issue_data, :issue_hooks)
+ issue_data = hook_data(issue, action)
+ hooks_scope = issue.confidential? ? :confidential_issue_hooks : :issue_hooks
+ issue.project.execute_hooks(issue_data, hooks_scope)
+ issue.project.execute_services(issue_data, hooks_scope)
end
end
end
diff --git a/app/services/issues/bulk_update_service.rb b/app/services/issues/bulk_update_service.rb
deleted file mode 100644
index 7e19a73f71a..00000000000
--- a/app/services/issues/bulk_update_service.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module Issues
- class BulkUpdateService < BaseService
- def execute
- issues_ids = params.delete(:issues_ids).split(",")
- issue_params = params
-
- %i(state_event milestone_id assignee_id add_label_ids remove_label_ids subscription_event).each do |key|
- issue_params.delete(key) unless issue_params[key].present?
- end
-
- issues = Issue.where(id: issues_ids)
-
- issues.each do |issue|
- next unless can?(current_user, :update_issue, issue)
-
- Issues::UpdateService.new(issue.project, current_user, issue_params).execute(issue)
- end
-
- {
- count: issues.count,
- success: !issues.count.zero?
- }
- end
- end
-end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 290742f1506..e57791f6818 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -83,7 +83,7 @@ module MergeRequests
closes_issue = "Closes ##{iid}"
if merge_request.description.present?
- merge_request.description += closes_issue.prepend("\n")
+ merge_request.description += closes_issue.prepend("\n\n")
else
merge_request.description = closes_issue
end
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index 2aab8c736d6..776530ac0a5 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -31,6 +31,14 @@ class TodoService
mark_pending_todos_as_done(issue, current_user)
end
+ # When we destroy an issue we should:
+ #
+ # * refresh the todos count cache for the current user
+ #
+ def destroy_issue(issue, current_user)
+ destroy_issuable(issue, current_user)
+ end
+
# When we reassign an issue we should:
#
# * create a pending todo for new assignee if issue is assigned
@@ -64,6 +72,14 @@ class TodoService
mark_pending_todos_as_done(merge_request, current_user)
end
+ # When we destroy a merge request we should:
+ #
+ # * refresh the todos count cache for the current user
+ #
+ def destroy_merge_request(merge_request, current_user)
+ destroy_issuable(merge_request, current_user)
+ end
+
# When we reassign a merge request we should:
#
# * creates a pending todo for new assignee if merge request is assigned
@@ -187,6 +203,10 @@ class TodoService
create_mention_todos(issuable.project, issuable, author)
end
+ def destroy_issuable(issuable, user)
+ user.update_todos_count_cache
+ end
+
def toggling_tasks?(issuable)
issuable.previous_changes.include?('description') &&
issuable.tasks? && issuable.updated_tasks.any?
diff --git a/app/views/admin/background_jobs/show.html.haml b/app/views/admin/background_jobs/show.html.haml
index 4f680b507c4..05855db963a 100644
--- a/app/views/admin/background_jobs/show.html.haml
+++ b/app/views/admin/background_jobs/show.html.haml
@@ -28,14 +28,10 @@
%th COMMAND
%tbody
- @sidekiq_processes.each do |process|
- - next unless process.match(/(sidekiq \d+\.\d+\.\d+.+$)/)
- - data = process.strip.split(' ')
%tr
%td= gitlab_config.user
- - 5.times do
- %td= data.shift
- %td= data.join(' ')
-
+ - parse_sidekiq_ps(process).each do |value|
+ %td= value
.clearfix
%p
%i.fa.fa-exclamation-circle
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index 742f9d7a433..3be7ed8432c 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,3 +1,3 @@
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
- new MemberExpirationDate();
+ new gl.MemberExpirationDate();
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index ce4536ebdc6..16c16cec137 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -7,277 +7,278 @@
Keyboard Shortcuts
%small
= link_to '(Show all)', '#', class: 'js-more-help-button'
- .modal-body.shortcuts-cheatsheet
- .col-lg-4
- %table.shortcut-mappings
- %tbody
- %tr
- %th
- %th Global Shortcuts
- %tr
- %td.shortcut
- .key s
- %td Focus Search
- %tr
- %td.shortcut
- .key f
- %td Focus Filter
- %tr
- %td.shortcut
- .key ?
- %td Show/hide this dialog
- %tr
- %td.shortcut
- - if browser.platform.mac?
- .key &#8984; shift p
- - else
- .key ctrl shift p
- %td Toggle Markdown preview
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-up
- %td Edit last comment (when focused on an empty textarea)
- %tbody
- %tr
- %th
- %th Project Files browsing
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-up
- %td Move selection up
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-down
- %td Move selection down
- %tr
- %td.shortcut
- .key enter
- %td Open Selection
- %tbody
- %tr
- %th
- %th Finding Project File
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-up
- %td Move selection up
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-down
- %td Move selection down
- %tr
- %td.shortcut
- .key enter
- %td Open Selection
- %tr
- %td.shortcut
- .key esc
- %td Go back
+ .modal-body
+ .row
+ .col-lg-4
+ %table.shortcut-mappings
+ %tbody
+ %tr
+ %th
+ %th Global Shortcuts
+ %tr
+ %td.shortcut
+ .key s
+ %td Focus Search
+ %tr
+ %td.shortcut
+ .key f
+ %td Focus Filter
+ %tr
+ %td.shortcut
+ .key ?
+ %td Show/hide this dialog
+ %tr
+ %td.shortcut
+ - if browser.platform.mac?
+ .key &#8984; shift p
+ - else
+ .key ctrl shift p
+ %td Toggle Markdown preview
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Edit last comment (when focused on an empty textarea)
+ %tbody
+ %tr
+ %th
+ %th Project Files browsing
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Move selection up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ %td Move selection down
+ %tr
+ %td.shortcut
+ .key enter
+ %td Open Selection
+ %tbody
+ %tr
+ %th
+ %th Finding Project File
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Move selection up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ %td Move selection down
+ %tr
+ %td.shortcut
+ .key enter
+ %td Open Selection
+ %tr
+ %td.shortcut
+ .key esc
+ %td Go back
- .col-lg-4
- %table.shortcut-mappings
- %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
- %tr
- %th
- %th Global Dashboard
- %tr
- %td.shortcut
- .key g
- .key a
- %td
- Go to the activity feed
- %tr
- %td.shortcut
- .key g
- .key p
- %td
- Go to projects
- %tr
- %td.shortcut
- .key g
- .key i
- %td
- Go to issues
- %tr
- %td.shortcut
- .key g
- .key m
- %td
- Go to merge requests
- %tbody
- %tr
- %th
- %th Project
- %tr
- %td.shortcut
- .key g
- .key p
- %td
- Go to the project's home page
- %tr
- %td.shortcut
- .key g
- .key e
- %td
- Go to the project's activity feed
- %tr
- %td.shortcut
- .key g
- .key f
- %td
- Go to files
- %tr
- %td.shortcut
- .key g
- .key c
- %td
- Go to commits
- %tr
- %td.shortcut
- .key g
- .key b
- %td
- Go to builds
- %tr
- %td.shortcut
- .key g
- .key n
- %td
- Go to network graph
- %tr
- %td.shortcut
- .key g
- .key g
- %td
- Go to graphs
- %tr
- %td.shortcut
- .key g
- .key i
- %td
- Go to issues
- %tr
- %td.shortcut
- .key g
- .key m
- %td
- Go to merge requests
- %tr
- %td.shortcut
- .key g
- .key s
- %td
- Go to snippets
- %tr
- %td.shortcut
- .key t
- %td Go to finding file
- %tr
- %td.shortcut
- .key i
- %td New issue
- .col-lg-4
- %table.shortcut-mappings
- %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
- %tr
- %th
- %th Network Graph
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-left
- \/
- .key h
- %td Scroll left
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-right
- \/
- .key l
- %td Scroll right
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-up
- \/
- .key k
- %td Scroll up
- %tr
- %td.shortcut
- .key
- %i.fa.fa-arrow-down
- \/
- .key j
- %td Scroll down
- %tr
- %td.shortcut
- .key
- shift
- %i.fa.fa-arrow-up
- \/
- .key
- shift k
- %td Scroll to top
- %tr
- %td.shortcut
- .key
- shift
- %i.fa.fa-arrow-down
- \/
- .key
- shift j
- %td Scroll to bottom
- %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
- %tr
- %th
- %th Issues
- %tr
- %td.shortcut
- .key a
- %td Change assignee
- %tr
- %td.shortcut
- .key m
- %td Change milestone
- %tr
- %td.shortcut
- .key r
- %td Reply (quoting selected text)
- %tr
- %td.shortcut
- .key e
- %td Edit issue
- %tr
- %td.shortcut
- .key l
- %td Change Label
- %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
- %tr
- %th
- %th Merge Requests
- %tr
- %td.shortcut
- .key a
- %td Change assignee
- %tr
- %td.shortcut
- .key m
- %td Change milestone
- %tr
- %td.shortcut
- .key r
- %td Reply (quoting selected text)
- %tr
- %td.shortcut
- .key e
- %td Edit merge request
- %tr
- %td.shortcut
- .key l
- %td Change Label
+ .col-lg-4
+ %table.shortcut-mappings
+ %tbody{ class: 'hidden-shortcut project', style: 'display:none' }
+ %tr
+ %th
+ %th Global Dashboard
+ %tr
+ %td.shortcut
+ .key g
+ .key a
+ %td
+ Go to the activity feed
+ %tr
+ %td.shortcut
+ .key g
+ .key p
+ %td
+ Go to projects
+ %tr
+ %td.shortcut
+ .key g
+ .key i
+ %td
+ Go to issues
+ %tr
+ %td.shortcut
+ .key g
+ .key m
+ %td
+ Go to merge requests
+ %tbody
+ %tr
+ %th
+ %th Project
+ %tr
+ %td.shortcut
+ .key g
+ .key p
+ %td
+ Go to the project's home page
+ %tr
+ %td.shortcut
+ .key g
+ .key e
+ %td
+ Go to the project's activity feed
+ %tr
+ %td.shortcut
+ .key g
+ .key f
+ %td
+ Go to files
+ %tr
+ %td.shortcut
+ .key g
+ .key c
+ %td
+ Go to commits
+ %tr
+ %td.shortcut
+ .key g
+ .key b
+ %td
+ Go to builds
+ %tr
+ %td.shortcut
+ .key g
+ .key n
+ %td
+ Go to network graph
+ %tr
+ %td.shortcut
+ .key g
+ .key g
+ %td
+ Go to graphs
+ %tr
+ %td.shortcut
+ .key g
+ .key i
+ %td
+ Go to issues
+ %tr
+ %td.shortcut
+ .key g
+ .key m
+ %td
+ Go to merge requests
+ %tr
+ %td.shortcut
+ .key g
+ .key s
+ %td
+ Go to snippets
+ %tr
+ %td.shortcut
+ .key t
+ %td Go to finding file
+ %tr
+ %td.shortcut
+ .key i
+ %td New issue
+ .col-lg-4
+ %table.shortcut-mappings
+ %tbody{ class: 'hidden-shortcut network', style: 'display:none' }
+ %tr
+ %th
+ %th Network Graph
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-left
+ \/
+ .key h
+ %td Scroll left
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-right
+ \/
+ .key l
+ %td Scroll right
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ \/
+ .key k
+ %td Scroll up
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-down
+ \/
+ .key j
+ %td Scroll down
+ %tr
+ %td.shortcut
+ .key
+ shift
+ %i.fa.fa-arrow-up
+ \/
+ .key
+ shift k
+ %td Scroll to top
+ %tr
+ %td.shortcut
+ .key
+ shift
+ %i.fa.fa-arrow-down
+ \/
+ .key
+ shift j
+ %td Scroll to bottom
+ %tbody{ class: 'hidden-shortcut issues', style: 'display:none' }
+ %tr
+ %th
+ %th Issues
+ %tr
+ %td.shortcut
+ .key a
+ %td Change assignee
+ %tr
+ %td.shortcut
+ .key m
+ %td Change milestone
+ %tr
+ %td.shortcut
+ .key r
+ %td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit issue
+ %tr
+ %td.shortcut
+ .key l
+ %td Change Label
+ %tbody{ class: 'hidden-shortcut merge_requests', style: 'display:none' }
+ %tr
+ %th
+ %th Merge Requests
+ %tr
+ %td.shortcut
+ .key a
+ %td Change assignee
+ %tr
+ %td.shortcut
+ .key m
+ %td Change milestone
+ %tr
+ %td.shortcut
+ .key r
+ %td Reply (quoting selected text)
+ %tr
+ %td.shortcut
+ .key e
+ %td Edit merge request
+ %tr
+ %td.shortcut
+ .key l
+ %td Change Label
diff --git a/app/views/import/base/create.js.haml b/app/views/import/base/create.js.haml
index 804ad88468f..8e929538351 100644
--- a/app/views/import/base/create.js.haml
+++ b/app/views/import/base/create.js.haml
@@ -1,23 +1,4 @@
-- if @already_been_taken
- :plain
- tr = $("tr#repo_#{@repo_id}")
- target_field = tr.find(".import-target")
- import_button = tr.find(".btn-import")
- origin_target = target_field.text()
- project_name = "#{@project_name}"
- origin_namespace = "#{@target_namespace}"
- target_field.empty()
- target_field.append("<p class='alert alert-danger'>This namespace already been taken! Please choose another one</p>")
- target_field.append("<input type='text' name='target_namespace' />")
- target_field.append("/" + project_name)
- target_field.data("project_name", project_name)
- target_field.find('input').prop("value", origin_namespace)
- import_button.enable().removeClass('is-loading')
-- elsif @access_denied
- :plain
- job = $("tr#repo_#{@repo_id}")
- job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>")
-- elsif @project.persisted?
+- if @project.persisted?
:plain
job = $("tr#repo_#{@repo_id}")
job.attr("id", "project_#{@project.id}")
diff --git a/app/views/import/base/unauthorized.js.haml b/app/views/import/base/unauthorized.js.haml
new file mode 100644
index 00000000000..36f8069c1f7
--- /dev/null
+++ b/app/views/import/base/unauthorized.js.haml
@@ -0,0 +1,14 @@
+:plain
+ tr = $("tr#repo_#{@repo_id}")
+ target_field = tr.find(".import-target")
+ import_button = tr.find(".btn-import")
+ origin_target = target_field.text()
+ project_name = "#{@project_name}"
+ origin_namespace = "#{@target_namespace.path}"
+ target_field.empty()
+ target_field.append("<p class='alert alert-danger'>This namespace has already been taken! Please choose another one.</p>")
+ target_field.append("<input type='text' name='target_namespace' />")
+ target_field.append("/" + project_name)
+ target_field.data("project_name", project_name)
+ target_field.find('input').prop("value", origin_namespace)
+ import_button.enable().removeClass('is-loading')
diff --git a/app/views/import/bitbucket/deploy_key.js.haml b/app/views/import/bitbucket/deploy_key.js.haml
new file mode 100644
index 00000000000..81b34ab5c9d
--- /dev/null
+++ b/app/views/import/bitbucket/deploy_key.js.haml
@@ -0,0 +1,3 @@
+:plain
+ job = $("tr#repo_#{@repo_id}")
+ job.find(".import-actions").html("<p class='alert alert-danger'>Access denied! Please verify you can add deploy keys to this repository.</p>")
diff --git a/app/views/import/bitbucket/status.html.haml b/app/views/import/bitbucket/status.html.haml
index 15dd98077c8..f8b4b107513 100644
--- a/app/views/import/bitbucket/status.html.haml
+++ b/app/views/import/bitbucket/status.html.haml
@@ -51,7 +51,7 @@
%td
= link_to "#{repo["owner"]}/#{repo["slug"]}", "https://bitbucket.org/#{repo["owner"]}/#{repo["slug"]}", target: "_blank"
%td.import-target
- = "#{repo["owner"]}/#{repo["slug"]}"
+ = import_project_target(repo['owner'], repo['slug'])
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 54ff1d27c67..bd3be20c4f8 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -45,7 +45,7 @@
%td
= github_project_link(repo.full_name)
%td.import-target
- = repo.full_name
+ = import_project_target(repo.owner.login, repo.name)
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
diff --git a/app/views/import/gitlab/status.html.haml b/app/views/import/gitlab/status.html.haml
index fcfc6fd37f4..d31fc2e6adb 100644
--- a/app/views/import/gitlab/status.html.haml
+++ b/app/views/import/gitlab/status.html.haml
@@ -45,7 +45,7 @@
%td
= link_to repo["path_with_namespace"], "https://gitlab.com/#{repo["path_with_namespace"]}", target: "_blank"
%td.import-target
- = repo["path_with_namespace"]
+ = import_project_target(repo['namespace']['path'], repo['name'])
%td.import-actions.job-status
= button_tag class: "btn btn-import js-add-to-import" do
Import
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index bf50633af24..4f7839a881f 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,5 +1,5 @@
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
- .sidebar-wrapper.nicescroll{ class: nav_sidebar_class }
+ .sidebar-wrapper.nicescroll
.sidebar-action-buttons
= link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
%span.sr-only Toggle navigation
diff --git a/app/views/projects/_merge_request_settings.html.haml b/app/views/projects/_merge_request_settings.html.haml
index 19b4249374b..80053dd501b 100644
--- a/app/views/projects/_merge_request_settings.html.haml
+++ b/app/views/projects/_merge_request_settings.html.haml
@@ -1,11 +1,14 @@
-%fieldset.builds-feature
- %h5.prepend-top-0
- Merge Requests
- .form-group
- .checkbox
- = f.label :only_allow_merge_if_build_succeeds do
- = f.check_box :only_allow_merge_if_build_succeeds
- %strong Only allow merge requests to be merged if the build succeeds
- .help-block
- Builds need to be configured to enable this feature.
- = link_to icon('question-circle'), help_page_path('workflow/merge_requests', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
+.merge-requests-feature
+ %fieldset.builds-feature
+ %hr
+ %h5.prepend-top-0
+ Merge Requests
+ .form-group
+ .checkbox
+ = f.label :only_allow_merge_if_build_succeeds do
+ = f.check_box :only_allow_merge_if_build_succeeds
+ %strong Only allow merge requests to be merged if the build succeeds
+ %br
+ %span.descr
+ Builds need to be configured to enable this feature.
+ = link_to icon('question-circle'), help_page_path('user/project/merge_requests/merge_when_build_succeeds', anchor: 'only-allow-merge-requests-to-be-merged-if-the-build-succeeds')
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index 377665b096f..5a98e258b22 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -11,7 +11,7 @@
%small= number_to_human_size @blob.size
.file-actions
= render "projects/blob/actions"
- .file-content.blame.code.js-syntax-highlight
+ .table-responsive.file-content.blame.code.js-syntax-highlight
%table
- current_line = 1
- @blame_groups.each do |blame_group|
@@ -19,6 +19,7 @@
%td.blame-commit
.commit
- commit = blame_group[:commit]
+ = author_avatar(commit, size: 36)
.commit-row-title
%strong
= link_to_gfm truncate(commit.title, length: 35), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "cdark"
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 5b0b58e087b..56306b05934 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -1,3 +1,6 @@
+- builds = @build.pipeline.builds.latest.to_a
+- statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
+
%aside.right-sidebar.right-sidebar-expanded.build-sidebar.js-build-sidebar
.block.build-sidebar-header.visible-xs-block.visible-sm-block.append-bottom-default
Build
@@ -11,40 +14,6 @@
%p.build-detail-row
#{@build.coverage}%
- - builds = @build.pipeline.builds.latest.to_a
- - statuses = ["failed", "pending", "running", "canceled", "success", "skipped"]
- - if builds.size > 1
- .dropdown.build-dropdown
- .build-light-text Stage
- %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
- %span.stage-selection More
- = icon('caret-down')
- %ul.dropdown-menu
- - builds.map(&:stage).uniq.each do |stage|
- %li
- %a.stage-item= stage
-
- .builds-container
- - statuses.each do |build_status|
- - builds.select{|build| build.status == build_status}.each do |build|
- .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
- = link_to namespace_project_build_path(@project.namespace, @project, build) do
- = icon('check')
- = ci_icon_for_status(build.status)
- %span
- - if build.name
- = build.name
- - else
- = build.id
-
- - if @build.retried?
- %li.active
- %a
- Build ##{@build.id}
- &middot;
- %i.fa.fa-warning
- This build was retried.
-
.blocks-container
- if can?(current_user, :read_build, @project) && (@build.artifacts? || @build.artifacts_expired?)
.block{ class: ("block-first" if !@build.coverage) }
@@ -76,7 +45,7 @@
.title
Build details
- if can?(current_user, :update_build, @build) && @build.retryable?
- = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right', method: :post
+ = link_to "Retry", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'pull-right retry-link', method: :post
- if @build.merge_request
%p.build-detail-row
%span.build-light-text Merge Request:
@@ -100,7 +69,7 @@
- elsif @build.runner
\##{@build.runner.id}
.btn-group.btn-group-justified{ role: :group }
- - if @build.has_trace?
+ - if @build.has_trace_file?
= link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default'
- if @build.active?
= link_to "Cancel", cancel_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-sm btn-default', method: :post
@@ -121,12 +90,13 @@
- if @build.trigger_request.variables
%p
- %span.build-light-text Variables:
+ %button.btn.group.btn-group-justified.reveal-variables Reveal Variables
- @build.trigger_request.variables.each do |key, value|
- %code
- #{key}=#{value}
+ .hide.js-build
+ .js-build-variable= key
+ .js-build-value= value
.block
.title
@@ -141,3 +111,35 @@
- @build.tag_list.each do |tag|
%span.label.label-primary
= tag
+
+ - if builds.size > 1
+ .dropdown.build-dropdown
+ .title Stage
+ %button.dropdown-menu-toggle{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.stage-selection More
+ = icon('caret-down')
+ %ul.dropdown-menu
+ - builds.map(&:stage).uniq.each do |stage|
+ %li
+ %a.stage-item= stage
+
+ .builds-container
+ - statuses.each do |build_status|
+ - builds.select{|build| build.status == build_status}.each do |build|
+ .build-job{class: ('active' if build == @build), data: {stage: build.stage}}
+ = link_to namespace_project_build_path(@project.namespace, @project, build) do
+ = icon('check')
+ = ci_icon_for_status(build.status)
+ %span
+ - if build.name
+ = build.name
+ - else
+ = build.id
+
+ - if @build.retried?
+ %li.active
+ %a
+ Build ##{@build.id}
+ &middot;
+ %i.fa.fa-warning
+ This build was retried.
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index d78888e9fe4..22db33498f1 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -3,11 +3,11 @@
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn has-tooltip' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
%div.count-with-arrow
%span.arrow
= link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index a1d79bdabda..bacc5708e4b 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -32,11 +32,11 @@
- if current_user.already_forked?(@project) && current_user.manageable_namespaces.size < 2
= link_to namespace_project_path(current_user, current_user.fork_of(@project)), title: 'Go to your fork', class: 'btn btn-new' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
- else
= link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn btn-new' do
= custom_icon('icon_fork')
- Fork
+ %span Fork
= render 'projects', projects: @forks
diff --git a/app/views/projects/hooks/_project_hook.html.haml b/app/views/projects/hooks/_project_hook.html.haml
index 3fcf1692e09..ceabe2eab3d 100644
--- a/app/views/projects/hooks/_project_hook.html.haml
+++ b/app/views/projects/hooks/_project_hook.html.haml
@@ -3,7 +3,7 @@
.col-md-8.col-lg-7
%strong.light-header= hook.url
%div
- - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
+ - %w(push_events tag_push_events issues_events confidential_issues_events note_events merge_requests_events build_events pipeline_events wiki_page_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray.deploy-project-label= trigger.titleize
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 79b14819865..851d4c06990 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -1,7 +1,7 @@
%li{ id: dom_id(issue), class: issue_css_classes(issue), url: issue_path(issue), data: { labels: issue.label_ids, id: issue.id } }
- - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
+ - if @bulk_edit
.issue-check
- = check_box_tag dom_id(issue,"selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
+ = check_box_tag dom_id(issue, "selected"), nil, false, 'data-id' => issue.id, class: "selected_issue"
.issue-title.title
%span.issue-title-text
diff --git a/app/views/projects/issues/_issues.html.haml b/app/views/projects/issues/_issues.html.haml
index f34f3c05737..a2c31c0b4c5 100644
--- a/app/views/projects/issues/_issues.html.haml
+++ b/app/views/projects/issues/_issues.html.haml
@@ -1,4 +1,4 @@
-%ul.content-list.issues-list
+%ul.content-list.issues-list.issuable-list
= render @issues
- if @issues.blank?
%li
diff --git a/app/views/projects/issues/_new_branch.html.haml b/app/views/projects/issues/_new_branch.html.haml
index 33556a1a2b3..c56b6cc11f5 100644
--- a/app/views/projects/issues/_new_branch.html.haml
+++ b/app/views/projects/issues/_new_branch.html.haml
@@ -1,6 +1,6 @@
- if can?(current_user, :push_code, @project)
.pull-right
- #new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
+ #new-branch.new-branch{'data-path' => can_create_branch_namespace_project_issue_path(@project.namespace, @project, @issue)}
= link_to '#', class: 'checking btn btn-grouped', disabled: 'disabled' do
= icon('spinner spin')
Checking branches
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 1a87045aa60..8da9f2100e9 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- @bulk_edit = can?(current_user, :admin_issue, @project)
+
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
= render "projects/issues/head"
diff --git a/app/views/projects/merge_requests/_merge_request.html.haml b/app/views/projects/merge_requests/_merge_request.html.haml
index 5029b365f93..31f8d0aeb5b 100644
--- a/app/views/projects/merge_requests/_merge_request.html.haml
+++ b/app/views/projects/merge_requests/_merge_request.html.haml
@@ -1,4 +1,8 @@
%li{ class: mr_css_classes(merge_request) }
+ - if @bulk_edit
+ .issue-check
+ = check_box_tag dom_id(merge_request, "selected"), nil, false, 'data-id' => merge_request.id, class: "selected_issue"
+
.merge-request-title.title
%span.merge-request-title-text
= link_to merge_request.title, merge_request_path(merge_request)
diff --git a/app/views/projects/merge_requests/_merge_requests.html.haml b/app/views/projects/merge_requests/_merge_requests.html.haml
index 446887774a4..fe82f751f53 100644
--- a/app/views/projects/merge_requests/_merge_requests.html.haml
+++ b/app/views/projects/merge_requests/_merge_requests.html.haml
@@ -1,4 +1,4 @@
-%ul.content-list.mr-list
+%ul.content-list.mr-list.issuable-list
= render @merge_requests
- if @merge_requests.blank?
%li
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 9d8b4cc56be..4b4d418e8ec 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -83,7 +83,7 @@
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
- .content-block.content-block-small.oneline-block
+ .content-block.content-block-small
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.row
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index ace275c689b..144b3a9c8c8 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -1,4 +1,6 @@
- @no_container = true
+- @bulk_edit = can?(current_user, :admin_merge_request, @project)
+
- page_title "Merge Requests"
= render "projects/issues/head"
= render 'projects/last_push'
diff --git a/app/views/projects/merge_requests/show/_how_to_merge.html.haml b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
index b727efaa6a6..f1d5441f9dd 100644
--- a/app/views/projects/merge_requests/show/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/show/_how_to_merge.html.haml
@@ -12,7 +12,7 @@
%pre.dark#merge-info-1
- if @merge_request.for_fork?
:preserve
- git fetch #{h @merge_request.source_project.http_url_to_repo} #{h @merge_request.source_branch}
+ git fetch #{h default_url_to_repo(@merge_request.source_project)} #{h @merge_request.source_branch}
git checkout -b #{h @merge_request.source_project_path}-#{h @merge_request.source_branch} FETCH_HEAD
- else
:preserve
@@ -47,8 +47,9 @@
Note that pushing to GitLab requires write access to this repository.
%p
%strong Tip:
- You can also checkout merge requests locally by
- %a{href: 'https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/workflow/merge_requests.md#checkout-merge-requests-locally', target: '_blank'} following these guidelines
+ = succeed '.' do
+ You can also checkout merge requests locally by
+ = link_to 'following these guidelines', help_page_path('user/project/merge_requests.md', anchor: "checkout-merge-requests-locally"), target: '_blank'
:javascript
$(function(){
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 2da70ce7137..00287f2d245 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -1,31 +1,60 @@
-- merge_request_diffs = @merge_request.merge_request_diffs.select_without_diff
-
-- if merge_request_diffs.size > 1
- .mr-version-switch
- Version:
- %span.dropdown.inline
+- if @merge_request_diffs.size > 1
+ .mr-version-controls
+ Changes between
+ %span.dropdown.inline.mr-version-dropdown
%a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
- %strong.monospace<
+ %strong
- if @merge_request_diff.latest?
- #{"latest"}
+ latest version
- else
- #{@merge_request_diff.head_commit.short_id}
+ version #{version_index(@merge_request_diff)}
%span.caret
%ul.dropdown-menu.dropdown-menu-selectable
- - merge_request_diffs.each do |merge_request_diff|
+ - @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
+ = link_to merge_request_version_path(@project, @merge_request, merge_request_diff), class: ('is-active' if merge_request_diff == @merge_request_diff) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
%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
+ - if @merge_request_diff.base_commit_sha
+ and
+ %span.dropdown.inline.mr-version-compare-dropdown
+ %a.btn-link.dropdown-toggle{ data: {toggle: :dropdown} }
+ %strong
+ - if @start_sha
+ version #{version_index(@start_version)}
+ - else
+ #{@merge_request.target_branch}
+ %span.caret
+ %ul.dropdown-menu.dropdown-menu-selectable
+ - @comparable_diffs.each do |merge_request_diff|
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff, merge_request_diff.head_commit_sha), class: ('is-active' if merge_request_diff == @start_version) do
+ %strong
+ - if merge_request_diff.latest?
+ latest version
+ - else
+ version #{version_index(merge_request_diff)}
+ .monospace #{short_sha(merge_request_diff.head_commit_sha)}
+ %small
+ = time_ago_with_tooltip(merge_request_diff.created_at)
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+ %strong
+ #{@merge_request.target_branch} (base)
+ .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+
+ - unless @merge_request_diff.latest? && !@start_sha
+ .prepend-top-10
= 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}
+ - if @start_sha
+ Comments are disabled because you're comparing two versions of this merge request.
+ - else
+ Comments are disabled because you're viewing an old version of this merge request.
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 0a1e2bb2cc6..fda0592dd41 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -55,7 +55,7 @@
= render 'bitbucket_import_modal'
%div
- if gitlab_import_enabled?
- = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless bitbucket_import_configured?}" do
+ = link_to status_import_gitlab_path, class: "btn import_gitlab #{'how_to_import_link' unless gitlab_import_configured?}" do
= icon('gitlab', text: 'GitLab.com')
- unless gitlab_import_configured?
= render 'gitlab_import_modal'
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 063e83a407a..5800ef7de48 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -10,6 +10,8 @@
- if @pipeline.duration
in
= time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_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/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 833954bc039..37e55dc72a3 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,3 +1,3 @@
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
- new MemberExpirationDate();
+ new gl.MemberExpirationDate();
diff --git a/app/views/projects/refs/logs_tree.js.haml b/app/views/projects/refs/logs_tree.js.haml
index 8ee2aef0e61..1141168f037 100644
--- a/app/views/projects/refs/logs_tree.js.haml
+++ b/app/views/projects/refs/logs_tree.js.haml
@@ -5,8 +5,8 @@
:plain
var row = $("table.table_#{@hex_path} tr.file_#{hexdigest(file_name)}");
- row.find("td.tree_time_ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
- row.find("td.tree_commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
+ row.find("td.tree-time-ago").html('#{escape_javascript time_ago_with_tooltip(commit.committed_date)}');
+ row.find("td.tree-commit").html('#{escape_javascript render("projects/tree/tree_commit_column", commit: commit)}');
- if @more_log_url
:plain
diff --git a/app/views/projects/tree/_blob_item.html.haml b/app/views/projects/tree/_blob_item.html.haml
index a3a4dba3fa4..ee417b58cbf 100644
--- a/app/views/projects/tree/_blob_item.html.haml
+++ b/app/views/projects/tree/_blob_item.html.haml
@@ -4,6 +4,6 @@
- file_name = blob_item.name
= link_to namespace_project_blob_path(@project.namespace, @project, tree_join(@id || @commit.id, blob_item.name)), title: file_name do
%span.str-truncated= file_name
- %td.tree_time_ago.cgray
- = render 'projects/tree/spinner'
- %td.hidden-xs.tree_commit
+ %td.hidden-xs.tree-commit
+ %td.tree-time-ago.cgray.text-right
+ = render 'projects/tree/spinner' \ No newline at end of file
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index ca5d2d7722a..0f7d629ab98 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -4,20 +4,20 @@
%thead
%tr
%th Name
- %th Last Update
- %th.hidden-xs.last-commit
- Last Commit
- .last-commit-content.hidden-sm
+ %th.hidden-xs
+ .pull-left Last Commit
+ .last-commit.hidden-sm.pull-left
+ &nbsp;
%i.fa.fa-angle-right
&nbsp;
%small.light
= link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
&ndash;
= 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'
-
+ %small.commit-history-link-spacer &#124;
+ = link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id), class: 'commit-history-link'
+ %th.text-right Last Update
- if @path.present?
%tr.tree-item
%td.tree-item-file-name
diff --git a/app/views/projects/tree/_tree_item.html.haml b/app/views/projects/tree/_tree_item.html.haml
index 9577696fc0d..1ccef6d52ab 100644
--- a/app/views/projects/tree/_tree_item.html.haml
+++ b/app/views/projects/tree/_tree_item.html.haml
@@ -4,6 +4,6 @@
- path = flatten_tree(tree_item)
= link_to namespace_project_tree_path(@project.namespace, @project, tree_join(@id || @commit.id, path)), title: path do
%span.str-truncated= path
- %td.tree_time_ago.cgray
- = render 'projects/tree/spinner'
- %td.hidden-xs.tree_commit
+ %td.hidden-xs.tree-commit
+ %td.tree-time-ago.text-right
+ = render 'projects/tree/spinner' \ No newline at end of file
diff --git a/app/views/projects/triggers/index.html.haml b/app/views/projects/triggers/index.html.haml
index 7f3de47d7df..f6e0b0a7c8a 100644
--- a/app/views/projects/triggers/index.html.haml
+++ b/app/views/projects/triggers/index.html.haml
@@ -4,65 +4,89 @@
.col-lg-3
%h4.prepend-top-0
= page_title
- %p
- Triggers can force a specific branch or tag to rebuild with an API call.
+ %p.prepend-top-20
+ Triggers can force a specific branch or tag to get rebuilt with an API call.
+ %p.append-bottom-0
+ = succeed '.' do
+ Learn more in the
+ = link_to 'triggers documentation', help_page_path('ci/triggers/README'), target: '_blank'
.col-lg-9
- %h5.prepend-top-0
- Your triggers
- - if @triggers.any?
- .table-responsive
- %table.table
- %thead
- %th Token
- %th Last used
- %th
- = render partial: 'trigger', collection: @triggers, as: :trigger
- - else
- %p.settings-message.text-center.append-bottom-default
- No triggers have been created yet. Add one using the button below.
+ .panel.panel-default
+ .panel-heading
+ %h4.panel-title
+ Manage your project's triggers
+ .panel-body
+ - if @triggers.any?
+ .table-responsive
+ %table.table
+ %thead
+ %th
+ %strong Token
+ %th
+ %strong Last used
+ %th
+ = render partial: 'trigger', collection: @triggers, as: :trigger
+ - else
+ %p.settings-message.text-center.append-bottom-default
+ No triggers have been created yet. Add one using the button below.
- = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
- = f.submit "Add Trigger", class: 'btn btn-success'
+ = form_for @trigger, url: url_for(controller: 'projects/triggers', action: 'create') do |f|
+ = f.submit "Add trigger", class: 'btn btn-success'
- %h5.prepend-top-default
- Use CURL
+ .panel-footer
- %p.light
- Copy the token above, set your branch or tag name, and that reference will be rebuilt.
+ %p
+ In the following examples, you can see the exact API call you need to
+ make in order to rebuild a specific
+ %code ref
+ (branch or tag) with a trigger token.
+ %p
+ All you need to do is replace the
+ %code TOKEN
+ and
+ %code REF_NAME
+ with the trigger token and the branch or tag name respectively.
- %pre
- :plain
- curl -X POST \
- -F token=TOKEN \
- -F ref=REF_NAME \
- #{builds_trigger_url(@project.id)}
- %h5.prepend-top-default
- Use .gitlab-ci.yml
+ %h5.prepend-top-default
+ Use cURL
- %p.light
- In the
- %code .gitlab-ci.yml
- of the dependent project, include the following snippet.
- The project will rebuild at the end of the build.
+ %p.light
+ Copy one of the tokens above, set your branch or tag name, and that
+ reference will be rebuilt.
- %pre
- :plain
- trigger:
- type: deploy
- script:
- - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
- %h5.prepend-top-default
- Pass build variables
+ %pre
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F ref=REF_NAME \
+ #{builds_trigger_url(@project.id)}
+ %h5.prepend-top-default
+ Use .gitlab-ci.yml
- %p.light
- Add
- %code variables[VARIABLE]=VALUE
- to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
+ %p.light
+ In the
+ %code .gitlab-ci.yml
+ of another project, include the following snippet.
+ The project will be rebuilt at the end of the build.
- %pre.append-bottom-0
- :plain
- curl -X POST \
- -F token=TOKEN \
- -F "ref=REF_NAME" \
- -F "variables[RUN_NIGHTLY_BUILD]=true" \
- #{builds_trigger_url(@project.id)}
+ %pre
+ :plain
+ trigger_build:
+ stage: deploy
+ script:
+ - "curl -X POST -F token=TOKEN -F ref=REF_NAME #{builds_trigger_url(@project.id)}"
+ %h5.prepend-top-default
+ Pass build variables
+
+ %p.light
+ Add
+ %code variables[VARIABLE]=VALUE
+ to an API request. Variable values can be used to distinguish between triggered builds and normal builds.
+
+ %pre.append-bottom-0
+ :plain
+ curl -X POST \
+ -F token=TOKEN \
+ -F "ref=REF_NAME" \
+ -F "variables[RUN_NIGHTLY_BUILD]=true" \
+ #{builds_trigger_url(@project.id)}
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 0f4f744a71f..93c4d5c3d30 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,9 +1,11 @@
+- boards_page = controller.controller_name == 'boards'
+
.issues-filters
.issues-details-filters.row-content-block.second-block
= form_tag page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search]), method: :get, class: 'filter-form js-filter-form' do
- if params[:issue_search].present?
= hidden_field_tag :issue_search, params[:issue_search]
- - if controller.controller_name == 'issues' && can?(current_user, :admin_issue, @project)
+ - if @bulk_edit
.check-all-holder
= check_box_tag "check_all_issues", nil, false,
class: "check_all_issues left"
@@ -26,8 +28,11 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown"
+ .filter-item.inline.reset-filters
+ %a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :issue_search])} Reset filters
+
.pull-right
- - if controller.controller_name == 'boards'
+ - if boards_page
#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)
@@ -42,9 +47,9 @@
- else
= render 'shared/sort_dropdown'
- - if controller.controller_name == 'issues'
+ - if @bulk_edit
.issues_bulk_update.hide
- = form_tag bulk_update_namespace_project_issues_path(@project.namespace, @project), method: :post, class: 'bulk-update' do
+ = form_tag [:bulk_update, @project.namespace.becomes(Namespace), @project, type], method: :post, class: 'bulk-update' do
.filter-item.inline
= dropdown_tag("Status", options: { toggle_class: "js-issue-status", title: "Change status", dropdown_class: "dropdown-menu-status dropdown-menu-selectable", data: { field_name: "update[state_event]" } } ) do
%ul
@@ -67,10 +72,10 @@
%li
%a{href: "#", data: {id: "unsubscribe"}} Unsubscribe
- = hidden_field_tag 'update[issues_ids]', []
+ = hidden_field_tag 'update[issuable_ids]', []
= hidden_field_tag :state_event, params[:state_event]
.filter-item.inline
- = button_tag "Update issues", class: "btn update_selected_issues btn-save"
+ = button_tag "Update #{type.to_s.humanize(capitalize: false)}", class: "btn update_selected_issues btn-save"
- if !@labels.nil?
.row-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index d2ec6c3ddef..5d659eb83a9 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -52,6 +52,13 @@
%p.light
This URL will be triggered when an issue is created/updated/merged
%li
+ = f.check_box :confidential_issues_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :confidential_issues_events, class: 'list-label' do
+ %strong Confidential Issues events
+ %p.light
+ This URL will be triggered when a confidential issue is created/updated/merged
+ %li
= f.check_box :merge_requests_events, class: 'pull-left'
.prepend-left-20
= f.label :merge_requests_events, class: 'list-label' do
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 80a3e731e1d..7be4a471579 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,7 +1,11 @@
-%ul.content-list
- = render partial: 'shared/snippets/snippet', collection: @snippets
- - if @snippets.empty?
- %li
- .nothing-here-block Nothing here.
+.snippets-list-holder
+ %ul.content-list
+ = render partial: 'shared/snippets/snippet', collection: @snippets
+ - if @snippets.empty?
+ %li
+ .nothing-here-block Nothing here.
-= paginate @snippets, theme: 'gitlab'
+ = paginate @snippets, theme: 'gitlab', remote: true
+
+:javascript
+ gl.SnippetsList();
diff --git a/app/workers/prune_old_events_worker.rb b/app/workers/prune_old_events_worker.rb
new file mode 100644
index 00000000000..5883cafe1d1
--- /dev/null
+++ b/app/workers/prune_old_events_worker.rb
@@ -0,0 +1,17 @@
+class PruneOldEventsWorker
+ include Sidekiq::Worker
+
+ def perform
+ # Contribution calendar shows maximum 12 months of events.
+ # Double nested query is used because MySQL doesn't allow DELETE subqueries
+ # on the same table.
+ Event.unscoped.where(
+ '(id IN (SELECT id FROM (?) ids_to_remove))',
+ Event.unscoped.where(
+ 'created_at < ?',
+ (12.months + 1.day).ago).
+ select(:id).
+ limit(10_000)).
+ delete_all
+ end
+end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 4a01b9e40fb..195108b921b 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -299,6 +299,9 @@ Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpire
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'
+Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
+Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
#
# GitLab Shell
diff --git a/config/initializers/gitlab_workhorse_secret.rb b/config/initializers/gitlab_workhorse_secret.rb
new file mode 100644
index 00000000000..ed54dc11098
--- /dev/null
+++ b/config/initializers/gitlab_workhorse_secret.rb
@@ -0,0 +1,8 @@
+begin
+ Gitlab::Workhorse.secret
+rescue
+ Gitlab::Workhorse.write_secret
+end
+
+# Try a second time. If it does not work this will raise.
+Gitlab::Workhorse.secret
diff --git a/config/routes.rb b/config/routes.rb
index 262a174437a..068c92d1400 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -747,6 +747,7 @@ Rails.application.routes.draw do
get :branch_to
get :update_branches
get :diff_for_path
+ post :bulk_update
end
resources :discussions, only: [], constraints: { id: /\h{40}/ } do
diff --git a/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb b/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb
new file mode 100644
index 00000000000..a27947212f6
--- /dev/null
+++ b/db/migrate/20160830203109_add_confidential_issues_events_to_web_hooks.rb
@@ -0,0 +1,15 @@
+class AddConfidentialIssuesEventsToWebHooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :web_hooks, :confidential_issues_events, :boolean, default: false, allow_null: false
+ end
+
+ def down
+ remove_column :web_hooks, :confidential_issues_events
+ end
+end
diff --git a/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb b/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb
new file mode 100644
index 00000000000..030e7c39350
--- /dev/null
+++ b/db/migrate/20160830211132_add_confidential_issues_events_to_services.rb
@@ -0,0 +1,15 @@
+class AddConfidentialIssuesEventsToServices < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :services, :confidential_issues_events, :boolean, default: true, allow_null: false
+ end
+
+ def down
+ remove_column :services, :confidential_issues_events
+ end
+end
diff --git a/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
new file mode 100644
index 00000000000..f1a1f001cb3
--- /dev/null
+++ b/db/migrate/20160901141443_set_confidential_issues_events_on_webhooks.rb
@@ -0,0 +1,15 @@
+class SetConfidentialIssuesEventsOnWebhooks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ update_column_in_batches(:web_hooks, :confidential_issues_events, true) do |table, query|
+ query.where(table[:issues_events].eq(true))
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb b/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb
new file mode 100644
index 00000000000..a80a57254dd
--- /dev/null
+++ b/db/migrate/20160902122721_drop_gitorious_field_from_application_settings.rb
@@ -0,0 +1,39 @@
+class DropGitoriousFieldFromApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # After the deploy the caches will be cold anyway
+ DOWNTIME = false
+
+ def up
+ require 'yaml'
+
+ import_sources = connection.execute('SELECT import_sources FROM application_settings;')
+ return unless import_sources.first # support empty databases
+
+ yaml = if Gitlab::Database.postgresql?
+ import_sources.values[0][0]
+ else
+ import_sources.first[0]
+ end
+
+ yaml = YAML.safe_load(yaml)
+ yaml.delete 'gitorious'
+
+ # No need for a WHERE clause as there is only one
+ connection.execute("UPDATE application_settings SET import_sources = #{update_yaml(yaml)}")
+ end
+
+ def down
+ # noop, gitorious still yields a 404 anyway
+ end
+
+ private
+
+ def connection
+ ActiveRecord::Base.connection
+ end
+
+ def update_yaml(yaml)
+ connection.quote(YAML.dump(yaml))
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index af6e74a4e25..5c283141084 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: 20160831223750) do
+ActiveRecord::Schema.define(version: 20160902122721) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -909,19 +909,20 @@ ActiveRecord::Schema.define(version: 20160831223750) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "active", default: false, null: false
+ t.boolean "active", default: false, null: false
t.text "properties"
- t.boolean "template", default: false
- t.boolean "push_events", default: true
- t.boolean "issues_events", default: true
- t.boolean "merge_requests_events", default: true
- t.boolean "tag_push_events", default: true
- t.boolean "note_events", default: true, null: false
- t.boolean "build_events", default: false, null: false
- t.string "category", default: "common", null: false
- t.boolean "default", default: false
- t.boolean "wiki_page_events", default: true
- t.boolean "pipeline_events", default: false, null: false
+ t.boolean "template", default: false
+ t.boolean "push_events", default: true
+ t.boolean "issues_events", default: true
+ t.boolean "merge_requests_events", default: true
+ t.boolean "tag_push_events", default: true
+ t.boolean "note_events", default: true, null: false
+ t.boolean "build_events", default: false, null: false
+ t.string "category", default: "common", null: false
+ t.boolean "default", default: false
+ t.boolean "wiki_page_events", default: true
+ t.boolean "pipeline_events", default: false, null: false
+ t.boolean "confidential_issues_events", default: true, null: false
end
add_index "services", ["project_id"], name: "index_services_on_project_id", using: :btree
@@ -1121,22 +1122,23 @@ ActiveRecord::Schema.define(version: 20160831223750) do
add_index "users_star_projects", ["user_id"], name: "index_users_star_projects_on_user_id", using: :btree
create_table "web_hooks", force: :cascade do |t|
- t.string "url", limit: 2000
+ t.string "url", limit: 2000
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.string "type", default: "ProjectHook"
+ t.string "type", default: "ProjectHook"
t.integer "service_id"
- t.boolean "push_events", default: true, null: false
- t.boolean "issues_events", default: false, null: false
- t.boolean "merge_requests_events", default: false, null: false
- t.boolean "tag_push_events", default: false
- t.boolean "note_events", default: false, null: false
- t.boolean "enable_ssl_verification", default: true
- t.boolean "build_events", default: false, null: false
- t.boolean "wiki_page_events", default: false, null: false
+ t.boolean "push_events", default: true, null: false
+ t.boolean "issues_events", default: false, null: false
+ t.boolean "merge_requests_events", default: false, null: false
+ t.boolean "tag_push_events", default: false
+ t.boolean "note_events", default: false, null: false
+ t.boolean "enable_ssl_verification", default: true
+ t.boolean "build_events", default: false, null: false
+ t.boolean "wiki_page_events", default: false, null: false
t.string "token"
- t.boolean "pipeline_events", default: false, null: false
+ t.boolean "pipeline_events", default: false, null: false
+ t.boolean "confidential_issues_events", default: false, null: false
end
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
diff --git a/doc/api/README.md b/doc/api/README.md
index 3e79cce0120..e12070dc1ce 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -11,9 +11,10 @@ following locations:
- [Award Emoji](award_emoji.md)
- [Branches](branches.md)
- [Builds](builds.md)
-- [Build triggers](build_triggers.md)
+- [Build Triggers](build_triggers.md)
- [Build Variables](build_variables.md)
- [Commits](commits.md)
+- [Deployments](deployments.md)
- [Deploy Keys](deploy_keys.md)
- [Groups](groups.md)
- [Group Access Requests](access_requests.md)
@@ -40,8 +41,9 @@ following locations:
- [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md)
- [Tags](tags.md)
-- [Users](users.md)
- [Todos](todos.md)
+- [Users](users.md)
+- [Validate CI configuration](ci/lint.md)
### Internal CI API
diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md
new file mode 100644
index 00000000000..0c96b3ee335
--- /dev/null
+++ b/doc/api/ci/lint.md
@@ -0,0 +1,49 @@
+# Validate the .gitlab-ci.yml
+
+> [Introduced][ce-5953] in GitLab 8.12.
+
+Checks if your .gitlab-ci.yml file is valid.
+
+```
+POST ci/lint
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------- | -------- | -------- |
+| `content` | string | yes | the .gitlab-ci.yaml content|
+
+```bash
+curl --header "Content-Type: application/json" https://gitlab.example.com/api/v3/ci/lint --data '{"content": "{ \"image\": \"ruby:2.1\", \"services\": [\"postgres\"], \"before_script\": [\"gem install bundler\", \"bundle install\", \"bundle exec rake db:create\"], \"variables\": {\"DB_NAME\": \"postgres\"}, \"types\": [\"test\", \"deploy\", \"notify\"], \"rspec\": { \"script\": \"rake spec\", \"tags\": [\"ruby\", \"postgres\"], \"only\": [\"branches\"]}}"}'
+```
+
+Be sure to copy paste the exact contents of `.gitlab-ci.yml` as YAML is very picky about indentation and spaces.
+
+Example responses:
+
+* Valid content:
+
+ ```json
+ {
+ "status": "valid",
+ "errors": []
+ }
+ ```
+
+* Invalid content:
+
+ ```json
+ {
+ "status": "invalid",
+ "errors": [
+ "variables config should be a hash of key value pairs"
+ ]
+ }
+ ```
+
+* Without the content attribute:
+
+ ```json
+ {
+ "error": "content is missing"
+ }
+ ```
diff --git a/doc/api/members.md b/doc/api/members.md
index fd6d728dad2..6535e9a7801 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -110,8 +110,8 @@ POST /projects/:id/members
| `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
-curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/members/:user_id?access_level=30
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/groups/:id/members
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "user_id=1&access_level=30" https://gitlab.example.com/api/v3/projects/:id/members
```
Example response:
diff --git a/doc/api/projects.md b/doc/api/projects.md
index a62aaee14d7..fe3c8709d13 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -514,7 +514,7 @@ invalid, 400 is returned.
### Fork project
-Forks a project into the user namespace of the authenticated user.
+Forks a project into the user namespace of the authenticated user or the one provided.
```
POST /projects/fork/:id
@@ -523,6 +523,7 @@ POST /projects/fork/:id
Parameters:
- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
+- `namespace` (optional) - The ID or path of the namespace that the project will be forked to
### Star a project
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index c134106bfd0..406396deaaa 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -1,17 +1,19 @@
# CI Examples
+A collection of `.gitlab-ci.yml` files is maintained at the [GitLab CI Yml project][gitlab-ci-templates].
+If your favorite programming language or framework are missing we would love your help by sending a merge request
+with a `.gitlab-ci.yml`.
+
+Apart from those, here is an collection of tutorials and guides on setting up your CI pipeline:
+
- [Testing a PHP application](php.md)
- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md)
- [Test a Scala application](test-scala-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
-- Help your favorite programming language and GitLab by sending a merge request
- with a guide for that language.
-
-## Outside the documentation
-
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
-- [A collection of useful .gitlab-ci.yml templates](https://gitlab.com/gitlab-org/gitlab-ci-yml)
+
+[gitlab-ci-templates][https://gitlab.com/gitlab-org/gitlab-ci-yml]
diff --git a/doc/ci/ssh_keys/README.md b/doc/ci/ssh_keys/README.md
index 7c0fb225dac..b858029d25e 100644
--- a/doc/ci/ssh_keys/README.md
+++ b/doc/ci/ssh_keys/README.md
@@ -30,7 +30,8 @@ This is the universal solution which works with any type of executor
## SSH keys when using the Docker executor
You will first need to create an SSH key pair. For more information, follow the
-instructions to [generate an SSH key](../../ssh/README.md).
+instructions to [generate an SSH key](../../ssh/README.md). Do not add a comment
+to the SSH key, or the `before_script` will prompt for a passphrase.
Then, create a new **Secret Variable** in your project settings on GitLab
following **Settings > Variables**. As **Key** add the name `SSH_PRIVATE_KEY`
diff --git a/doc/ci/triggers/img/builds_page.png b/doc/ci/triggers/img/builds_page.png
index 2dee8ee6107..c2cf4b1852c 100644
--- a/doc/ci/triggers/img/builds_page.png
+++ b/doc/ci/triggers/img/builds_page.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_single_build.png b/doc/ci/triggers/img/trigger_single_build.png
index baf3fc183d8..fa86f0fee3d 100644
--- a/doc/ci/triggers/img/trigger_single_build.png
+++ b/doc/ci/triggers/img/trigger_single_build.png
Binary files differ
diff --git a/doc/ci/triggers/img/trigger_variables.png b/doc/ci/triggers/img/trigger_variables.png
index 908355c33a5..b2fcc65d304 100644
--- a/doc/ci/triggers/img/trigger_variables.png
+++ b/doc/ci/triggers/img/trigger_variables.png
Binary files differ
diff --git a/doc/ci/triggers/img/triggers_page.png b/doc/ci/triggers/img/triggers_page.png
index 69cec5cdebf..438f285ae2d 100644
--- a/doc/ci/triggers/img/triggers_page.png
+++ b/doc/ci/triggers/img/triggers_page.png
Binary files differ
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 4a7c21f811d..c32831d3aaa 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -76,8 +76,8 @@ export CI_RUNNER_DESCRIPTION="my runner"
export CI_RUNNER_TAGS="docker, linux"
export CI_SERVER="yes"
export CI_SERVER_NAME="GitLab"
-export CI_SERVER_REVISION="8.9.0"
-export CI_SERVER_VERSION="70606bf"
+export CI_SERVER_REVISION="70606bf"
+export CI_SERVER_VERSION="8.9.0"
```
### YAML-defined variables
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 58d5306f12a..ff4c8ddc54b 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -6,50 +6,6 @@ GitLab Runner to manage your project's builds.
If you want a quick introduction to GitLab CI, follow our
[quick start guide](../quick_start/README.md).
----
-
-<!-- START doctoc generated TOC please keep comment here to allow auto update -->
-<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
-**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
-
-- [.gitlab-ci.yml](#gitlab-ci-yml)
- - [image and services](#image-and-services)
- - [before_script](#before_script)
- - [after_script](#after_script)
- - [stages](#stages)
- - [types](#types)
- - [variables](#variables)
- - [cache](#cache)
- - [cache:key](#cache-key)
-- [Jobs](#jobs)
- - [script](#script)
- - [stage](#stage)
- - [only and except](#only-and-except)
- - [job variables](#job-variables)
- - [tags](#tags)
- - [allow_failure](#allow_failure)
- - [when](#when)
- - [Manual actions](#manual-actions)
- - [environment](#environment)
- - [artifacts](#artifacts)
- - [artifacts:name](#artifacts-name)
- - [artifacts:when](#artifacts-when)
- - [artifacts:expire_in](#artifacts-expire_in)
- - [dependencies](#dependencies)
- - [before_script and after_script](#before_script-and-after_script)
-- [Git Strategy](#git-strategy)
-- [Shallow cloning](#shallow-cloning)
-- [Hidden keys](#hidden-keys)
-- [Special YAML features](#special-yaml-features)
- - [Anchors](#anchors)
-- [Validate the .gitlab-ci.yml](#validate-the-gitlab-ci-yml)
-- [Skipping builds](#skipping-builds)
-- [Examples](#examples)
-
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
-
----
-
## .gitlab-ci.yml
From version 7.12, GitLab CI uses a [YAML](https://en.wikipedia.org/wiki/YAML)
@@ -183,7 +139,7 @@ Alias for [stages](#stages).
Introduced in GitLab Runner v0.5.0.
GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
-build environment. The variables are stored in the git repository and are meant
+build environment. The variables are stored in the Git repository and are meant
to store non-sensitive project configuration, for example:
```yaml
@@ -198,6 +154,8 @@ thus allowing to fine tune them.
Variables can be also defined on [job level](#job-variables).
+[Learn more about variables.](../variables/README.md)
+
### cache
>**Note:**
@@ -1086,6 +1044,13 @@ test:mysql:
You can see that the hidden keys are conveniently used as templates.
+## Triggers
+
+Triggers can be used to force a rebuild of a specific branch, tag or commit,
+with an API call.
+
+[Read more in the triggers documentation.](../triggers/README.md)
+
## Validate the .gitlab-ci.yml
Each instance of GitLab CI has an embedded debug tool called Lint.
diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md
index 57136ac5c39..ff10a98e8f5 100644
--- a/doc/gitlab-basics/add-file.md
+++ b/doc/gitlab-basics/add-file.md
@@ -25,7 +25,3 @@ Add all the information that you'd like to include in your file:
Add a commit message based on what you just added and then click on "commit changes":
![Commit changes](basicsimages/commit_changes.png)
-
-### Note
-Besides its regular files, every directory needs a README.md or README.html file which works like an index, telling
-what the directory is about. It's the first document you'll find when you open a directory.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 9522c3e7170..df98655c396 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -400,7 +400,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.11
+ sudo -u git -H git checkout v0.8.1
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 9799e0a3730..766a7119943 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -86,7 +86,7 @@ if your available memory changes.
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
-## Gitlab Runner
+## GitLab Runner
We strongly advise against installing GitLab Runner on the same machine you plan
to install GitLab on. Depending on how you decide to configure GitLab Runner and
diff --git a/doc/intro/README.md b/doc/intro/README.md
index 1850031eb26..71fef50ceb4 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -23,9 +23,9 @@ Create merge requests and review code.
- [Fork a project and contribute to it](../workflow/forking_workflow.md)
- [Create a new merge request](../gitlab-basics/add-merge-request.md)
- [Automatically close issues from merge requests](../customization/issue_closing.md)
-- [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md)
-- [Revert any commit](../workflow/revert_changes.md)
-- [Cherry-pick any commit](../workflow/cherry_pick_changes.md)
+- [Automatically merge when your builds succeed](../user/project/merge_requests/merge_when_build_succeeds.md)
+- [Revert any commit](../user/project/merge_requests/revert_changes.md)
+- [Cherry-pick any commit](../user/project/merge_requests/cherry_pick_changes.md)
## Test and Deploy
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index c8ca42f97bc..011c2b0e969 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -82,7 +82,7 @@ GitLab 8.1.
```bash
cd /home/git/gitlab-workhorse
sudo -u git -H git fetch --all
-sudo -u git -H git checkout v0.7.11
+sudo -u git -H git checkout v0.8.1
sudo -u git -H make
```
diff --git a/doc/user/project/merge_requests.md b/doc/user/project/merge_requests.md
new file mode 100644
index 00000000000..f79535d1542
--- /dev/null
+++ b/doc/user/project/merge_requests.md
@@ -0,0 +1,166 @@
+# Merge Requests
+
+Merge requests allow you to exchange changes you made to source code and
+collaborate with other people on the same project.
+
+## Authorization for merge requests
+
+There are two main ways to have a merge request flow with GitLab:
+
+1. Working with [protected branches][] in a single repository
+1. Working with forks of an authoritative project
+
+[Learn more about the authorization for merge requests.](merge_requests/authorization_for_merge_requests.md)
+
+## Cherry-pick changes
+
+Cherry-pick any commit in the UI by simply clicking the **Cherry-pick** button
+in a merged merge requests or a commit.
+
+[Learn more about cherry-picking changes.](merge_requests/cherry_pick_changes.md)
+
+## Merge when build succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or
+more CI builds running, you can set it to be merged automatically when all
+builds succeed. This way, you don't have to wait for the builds to finish and
+remember to merge the request manually.
+
+[Learn more about merging when build succeeds.](merge_requests/merge_when_build_succeeds.md)
+
+## Resolve discussion comments in merge requests reviews
+
+Keep track of the progress during a code review with resolving comments.
+Resolving comments prevents you from forgetting to address feedback and lets
+you hide discussions that are no longer relevant.
+
+[Read more about resolving discussion comments in merge requests reviews.](merge_requests/merge_request_discussion_resolution.md)
+
+## Resolve conflicts
+
+When a merge request has conflicts, GitLab may provide the option to resolve
+those conflicts in the GitLab UI.
+
+[Learn more about resolving merge conflicts in the UI.](merge_requests/resolve_conflicts.md)
+
+## Revert changes
+
+GitLab implements Git's powerful feature to revert any commit with introducing
+a **Revert** button in merge requests and commit details.
+
+[Learn more about reverting changes in the UI](merge_requests/revert_changes.md)
+
+## Merge requests versions
+
+Every time you push to a branch that is tied to a merge request, a new version
+of merge request diff is created. When you visit a merge request that contains
+more than one pushes, you can select and compare the versions of those merge
+request diffs.
+
+[Read more about the merge requests versions.](merge_requests/versions.md)
+
+## Work In Progress merge requests
+
+To prevent merge requests from accidentally being accepted before they're
+completely ready, GitLab blocks the "Accept" button for merge requests that
+have been marked as a **Work In Progress**.
+
+[Learn more about settings a merge request as "Work In Progress".](merge_requests/work_in_progress_merge_requests.md)
+
+## Ignore whitespace changes in Merge Request diff view
+
+If you click the **Hide whitespace changes** button, you can see the diff
+without whitespace changes (if there are any). This is also working when on a
+specific commit page.
+
+![MR diff](merge_requests/img/merge_request_diff.png)
+
+>**Tip:**
+You can append `?w=1` while on the diffs page of a merge request to ignore any
+whitespace changes.
+
+## Tips
+
+Here are some tips that will help you be more efficient with merge requests in
+the command line.
+
+> **Note:**
+This section might move in its own document in the future.
+
+### Checkout merge requests locally
+
+A merge request contains all the history from a repository, plus the additional
+commits added to the branch associated with the merge request. Here's a few
+tricks to checkout a merge request locally.
+
+#### Checkout 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. For example, to check out the merge request with ID 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.
+
+#### Checkout locally by modifying `.git/config` for a given repository
+
+Locate the section for your GitLab remote in the `.git/config` file. It looks
+like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-ce.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+```
+
+You can open the file with:
+
+```
+git config -e
+```
+
+Now add the following line to the above section:
+
+```
+fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+In the end, it should look like this:
+
+```
+[remote "origin"]
+ url = https://gitlab.com/gitlab-org/gitlab-ce.git
+ fetch = +refs/heads/*:refs/remotes/origin/*
+ fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
+```
+
+Now you can fetch all the merge requests:
+
+```
+git fetch origin
+
+...
+From https://gitlab.com/gitlab-org/gitlab-ce.git
+ * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
+ * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
+...
+```
+
+And to check out a particular merge request:
+
+```
+git checkout origin/merge-requests/1
+```
+
+[protected branches]: protected_branches.md
diff --git a/doc/user/project/merge_requests/authorization_for_merge_requests.md b/doc/user/project/merge_requests/authorization_for_merge_requests.md
new file mode 100644
index 00000000000..59b3fe7242c
--- /dev/null
+++ b/doc/user/project/merge_requests/authorization_for_merge_requests.md
@@ -0,0 +1,56 @@
+# Authorization for Merge requests
+
+There are two main ways to have a merge request flow with GitLab:
+
+1. Working with [protected branches] in a single repository.
+1. Working with forks of an authoritative project.
+
+## Protected branch flow
+
+With the protected branch flow everybody works within the same GitLab project.
+
+The project maintainers get Master access and the regular developers get
+Developer access.
+
+The maintainers mark the authoritative branches as 'Protected'.
+
+The developers push feature branches to the project and create merge requests
+to have their feature branches reviewed and merged into one of the protected
+branches.
+
+By default, only users with Master access can merge changes into a protected
+branch.
+
+**Advantages**
+
+- Fewer projects means less clutter.
+- Developers need to consider only one remote repository.
+
+**Disadvantages**
+
+- Manual setup of protected branch required for each new project
+
+## Forking workflow
+
+With the forking workflow the maintainers get Master access and the regular
+developers get Reporter access to the authoritative repository, which prohibits
+them from pushing any changes to it.
+
+Developers create forks of the authoritative project and push their feature
+branches to their own forks.
+
+To get their changes into master they need to create a merge request across
+forks.
+
+**Advantages**
+
+- In an appropriately configured GitLab group, new projects automatically get
+ the required access restrictions for regular developers: fewer manual steps
+ to configure authorization for new projects.
+
+**Disadvantages**
+
+- The project need to keep their forks up to date, which requires more advanced
+ Git skills (managing multiple remotes).
+
+[protected branches]: ../protected_branches.md
diff --git a/doc/user/project/merge_requests/cherry_pick_changes.md b/doc/user/project/merge_requests/cherry_pick_changes.md
new file mode 100644
index 00000000000..64b94d81024
--- /dev/null
+++ b/doc/user/project/merge_requests/cherry_pick_changes.md
@@ -0,0 +1,52 @@
+# Cherry-pick changes
+
+> [Introduced][ce-3514] in GitLab 8.7.
+
+---
+
+GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
+with introducing a **Cherry-pick** button in Merge Requests and commit details.
+
+## Cherry-picking a Merge Request
+
+After the Merge Request has been merged, a **Cherry-pick** button will be available
+to cherry-pick the changes introduced by that Merge Request:
+
+![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
+
+---
+
+You can cherry-pick the changes directly into the selected branch or you can opt to
+create a new Merge Request with the cherry-pick changes:
+
+![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
+
+## Cherry-picking a Commit
+
+You can cherry-pick a Commit from the Commit details page:
+
+![Cherry-pick commit](img/cherry_pick_changes_commit.png)
+
+---
+
+Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
+directly into the target branch or create a new Merge Request to cherry-pick the
+changes:
+
+![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
+
+---
+
+Please note that when cherry-picking merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to cherry-pick a merge commit using the second parent as the
+mainline:
+
+```bash
+git cherry-pick -m 2 7a39eb0
+```
+
+[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
+[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
diff --git a/doc/workflow/img/cherry_pick_changes_commit.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
index 7fb68cc9e9b..7fb68cc9e9b 100644
--- a/doc/workflow/img/cherry_pick_changes_commit.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_commit_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
index 5267e04562f..5267e04562f 100644
--- a/doc/workflow/img/cherry_pick_changes_commit_modal.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_commit_modal.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
index 975fb13e463..975fb13e463 100644
--- a/doc/workflow/img/cherry_pick_changes_mr.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr_modal.png b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
index 6c003bacbe3..6c003bacbe3 100644
--- a/doc/workflow/img/cherry_pick_changes_mr_modal.png
+++ b/doc/user/project/merge_requests/img/cherry_pick_changes_mr_modal.png
Binary files differ
diff --git a/doc/workflow/merge_requests/commit_compare.png b/doc/user/project/merge_requests/img/commit_compare.png
index 0e4a2b23c04..0e4a2b23c04 100644
--- a/doc/workflow/merge_requests/commit_compare.png
+++ b/doc/user/project/merge_requests/img/commit_compare.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_request_diff.png b/doc/user/project/merge_requests/img/merge_request_diff.png
new file mode 100644
index 00000000000..06ee4908edc
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_request_diff.png
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds/enable.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
index b86e6d7b3fd..b86e6d7b3fd 100644
--- a/doc/workflow/merge_when_build_succeeds/enable.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_enable.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
new file mode 100644
index 00000000000..6b9756b7418
--- /dev/null
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_msg.png
Binary files differ
diff --git a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
index 18bebf5fe92..18bebf5fe92 100644
--- a/doc/workflow/merge_requests/only_allow_merge_if_build_succeeds.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_only_if_succeeds_settings.png
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds/status.png b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
index f3ea61d8147..f3ea61d8147 100644
--- a/doc/workflow/merge_when_build_succeeds/status.png
+++ b/doc/user/project/merge_requests/img/merge_when_build_succeeds_status.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_commit.png b/doc/user/project/merge_requests/img/revert_changes_commit.png
index e7194fc3504..e7194fc3504 100644
--- a/doc/workflow/img/revert_changes_commit.png
+++ b/doc/user/project/merge_requests/img/revert_changes_commit.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_commit_modal.png b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
index c660ec7eaec..c660ec7eaec 100644
--- a/doc/workflow/img/revert_changes_commit_modal.png
+++ b/doc/user/project/merge_requests/img/revert_changes_commit_modal.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_mr.png b/doc/user/project/merge_requests/img/revert_changes_mr.png
index 3002f0ac1c5..3002f0ac1c5 100644
--- a/doc/workflow/img/revert_changes_mr.png
+++ b/doc/user/project/merge_requests/img/revert_changes_mr.png
Binary files differ
diff --git a/doc/workflow/img/revert_changes_mr_modal.png b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
index c6aaeecc8a6..c6aaeecc8a6 100644
--- a/doc/workflow/img/revert_changes_mr_modal.png
+++ b/doc/user/project/merge_requests/img/revert_changes_mr_modal.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/versions.png b/doc/user/project/merge_requests/img/versions.png
new file mode 100644
index 00000000000..f279ccd7ce3
--- /dev/null
+++ b/doc/user/project/merge_requests/img/versions.png
Binary files differ
diff --git a/doc/workflow/wip_merge_requests/blocked_accept_button.png b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
index 89c458aa8d9..89c458aa8d9 100644
--- a/doc/workflow/wip_merge_requests/blocked_accept_button.png
+++ b/doc/user/project/merge_requests/img/wip_blocked_accept_button.png
Binary files differ
diff --git a/doc/workflow/wip_merge_requests/mark_as_wip.png b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
index 9c37354a653..9c37354a653 100644
--- a/doc/workflow/wip_merge_requests/mark_as_wip.png
+++ b/doc/user/project/merge_requests/img/wip_mark_as_wip.png
Binary files differ
diff --git a/doc/workflow/wip_merge_requests/unmark_as_wip.png b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
index 31f7326beb0..31f7326beb0 100644
--- a/doc/workflow/wip_merge_requests/unmark_as_wip.png
+++ b/doc/user/project/merge_requests/img/wip_unmark_as_wip.png
Binary files differ
diff --git a/doc/user/project/merge_requests/merge_when_build_succeeds.md b/doc/user/project/merge_requests/merge_when_build_succeeds.md
new file mode 100644
index 00000000000..011f9cbc381
--- /dev/null
+++ b/doc/user/project/merge_requests/merge_when_build_succeeds.md
@@ -0,0 +1,46 @@
+# Merge When Build Succeeds
+
+When reviewing a merge request that looks ready to merge but still has one or
+more CI builds running, you can set it to be merged automatically when all
+builds succeed. This way, you don't have to wait for the builds to finish and
+remember to merge the request manually.
+
+![Enable](img/merge_when_build_succeeds_enable.png)
+
+When you hit the "Merge When Build Succeeds" button, the status of the merge
+request will be updated to represent the impending merge. If you cannot wait
+for the build to succeed and want to merge immediately, this option is available
+in the dropdown menu on the right of the main button.
+
+Both team developers and the author of the merge request have the option to
+cancel the automatic merge if they find a reason why it shouldn't be merged
+after all.
+
+![Status](img/merge_when_build_succeeds_status.png)
+
+When the build succeeds, the merge request will automatically be merged. When
+the build fails, the author gets a chance to retry any failed builds, or to
+push new commits to fix the failure.
+
+When the builds are retried and succeed on the second try, the merge request
+will automatically be merged after all. When the merge request is updated with
+new commits, the automatic merge is automatically canceled to allow the new
+changes to be reviewed.
+
+## Only allow merge requests to be merged if the build succeeds
+
+> **Note:**
+You need to have builds configured to enable this feature.
+
+You can prevent merge requests from being merged if their build did not succeed.
+
+Navigate to your project's settings page, select the
+**Only allow merge requests to be merged if the build succeeds** check box and
+hit **Save** for the changes to take effect.
+
+![Only allow merge if build succeeds settings](img/merge_when_build_succeeds_only_if_succeeds_settings.png)
+
+From now on, every time the build fails you will not be able to merge the merge
+request from the UI, until you make the build pass.
+
+![Only allow merge if build succeeds msg](img/merge_when_build_succeeds_only_if_succeeds_msg.png)
diff --git a/doc/user/project/merge_requests/revert_changes.md b/doc/user/project/merge_requests/revert_changes.md
new file mode 100644
index 00000000000..5ead9f4177f
--- /dev/null
+++ b/doc/user/project/merge_requests/revert_changes.md
@@ -0,0 +1,64 @@
+# Reverting changes
+
+> [Introduced][ce-1990] in GitLab 8.5.
+
+---
+
+GitLab implements Git's powerful feature to [revert any commit][git-revert]
+with introducing a **Revert** button in Merge Requests and commit details.
+
+## Reverting a Merge Request
+
+_**Note:** The **Revert** button will only be available for Merge Requests
+created since GitLab 8.5. However, you can still revert a Merge Request
+by reverting the merge commit from the list of Commits page._
+
+After the Merge Request has been merged, a **Revert** button will be available
+to revert the changes introduced by that Merge Request:
+
+![Revert Merge Request](img/revert_changes_mr.png)
+
+---
+
+You can revert the changes directly into the selected branch or you can opt to
+create a new Merge Request with the revert changes:
+
+![Revert Merge Request modal](img/revert_changes_mr_modal.png)
+
+---
+
+After the Merge Request has been reverted, the **Revert** button will not be
+available anymore.
+
+## Reverting a Commit
+
+You can revert a Commit from the Commit details page:
+
+![Revert commit](img/revert_changes_commit.png)
+
+---
+
+Similar to reverting a Merge Request, you can opt to revert the changes
+directly into the target branch or create a new Merge Request to revert the
+changes:
+
+![Revert commit modal](img/revert_changes_commit_modal.png)
+
+---
+
+After the Commit has been reverted, the **Revert** button will not be available
+anymore.
+
+Please note that when reverting merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to revert a merge commit using the second parent as the
+mainline:
+
+```bash
+git revert -m 2 7a39eb0
+```
+
+[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request"
+[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation"
diff --git a/doc/user/project/merge_requests/versions.md b/doc/user/project/merge_requests/versions.md
new file mode 100644
index 00000000000..a6aa4b47835
--- /dev/null
+++ b/doc/user/project/merge_requests/versions.md
@@ -0,0 +1,28 @@
+# Merge requests versions
+
+> Will be [introduced][ce-5467] in GitLab 8.12.
+
+Every time you push to a branch that is tied to a merge request, a new version
+of merge request diff is created. When you visit a merge request that contains
+more than one pushes, you can select and compare the versions of those merge
+request diffs.
+
+By default, the latest version of changes is shown. However, you
+can select an older one from version dropdown.
+
+![Merge Request Versions](img/versions.png)
+
+You can also compare the merge request version with older one to see what is
+changed since then.
+
+Please note that comments are disabled while viewing outdated merge versions
+or comparing to versions other than base.
+
+---
+
+>**Note:**
+Merge request versions are based on push not on commit. So, if you pushed 5
+commits in a single push, it will be a single option in the dropdown. If you
+pushed 5 times, that will count for 5 options.
+
+[ce-5467]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5467
diff --git a/doc/user/project/merge_requests/work_in_progress_merge_requests.md b/doc/user/project/merge_requests/work_in_progress_merge_requests.md
new file mode 100644
index 00000000000..546c8bdc5e5
--- /dev/null
+++ b/doc/user/project/merge_requests/work_in_progress_merge_requests.md
@@ -0,0 +1,17 @@
+# "Work In Progress" Merge Requests
+
+To prevent merge requests from accidentally being accepted before they're
+completely ready, GitLab blocks the "Accept" button for merge requests that
+have been marked a **Work In Progress**.
+
+![Blocked Accept Button](img/wip_blocked_accept_button.png)
+
+To mark a merge request a Work In Progress, simply start its title with `[WIP]`
+or `WIP:`.
+
+![Mark as WIP](img/wip_mark_as_wip.png)
+
+To allow a Work In Progress merge request to be accepted again when it's ready,
+simply remove the `WIP` prefix.
+
+![Unark as WIP](img/wip_unmark_as_wip.png)
diff --git a/doc/workflow/img/web_editor_new_branch_dropdown.png b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png
index a8e635d2faf..a8e635d2faf 100644
--- a/doc/workflow/img/web_editor_new_branch_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_branch_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_branch_page.png b/doc/user/project/repository/img/web_editor_new_branch_page.png
index 7f36b7faf63..7f36b7faf63 100644
--- a/doc/workflow/img/web_editor_new_branch_page.png
+++ b/doc/user/project/repository/img/web_editor_new_branch_page.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_directory_dialog.png b/doc/user/project/repository/img/web_editor_new_directory_dialog.png
index d16e3c67116..d16e3c67116 100644
--- a/doc/workflow/img/web_editor_new_directory_dialog.png
+++ b/doc/user/project/repository/img/web_editor_new_directory_dialog.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_directory_dropdown.png b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png
index c8d77b16ee8..c8d77b16ee8 100644
--- a/doc/workflow/img/web_editor_new_directory_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_directory_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_file_dropdown.png b/doc/user/project/repository/img/web_editor_new_file_dropdown.png
index 3fcb91c9b93..3fcb91c9b93 100644
--- a/doc/workflow/img/web_editor_new_file_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_file_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_file_editor.png b/doc/user/project/repository/img/web_editor_new_file_editor.png
index 21c340b9288..21c340b9288 100644
--- a/doc/workflow/img/web_editor_new_file_editor.png
+++ b/doc/user/project/repository/img/web_editor_new_file_editor.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_push_widget.png b/doc/user/project/repository/img/web_editor_new_push_widget.png
index c7738a4c930..c7738a4c930 100644
--- a/doc/workflow/img/web_editor_new_push_widget.png
+++ b/doc/user/project/repository/img/web_editor_new_push_widget.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_tag_dropdown.png b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png
index ac7415009b3..ac7415009b3 100644
--- a/doc/workflow/img/web_editor_new_tag_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_new_tag_dropdown.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_new_tag_page.png b/doc/user/project/repository/img/web_editor_new_tag_page.png
index 231e1a13fc0..231e1a13fc0 100644
--- a/doc/workflow/img/web_editor_new_tag_page.png
+++ b/doc/user/project/repository/img/web_editor_new_tag_page.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_start_new_merge_request.png b/doc/user/project/repository/img/web_editor_start_new_merge_request.png
index 2755501dfd1..2755501dfd1 100644
--- a/doc/workflow/img/web_editor_start_new_merge_request.png
+++ b/doc/user/project/repository/img/web_editor_start_new_merge_request.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png
new file mode 100644
index 00000000000..4efc51cc423
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_buttons.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png
new file mode 100644
index 00000000000..67190c58823
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_first_file.png
Binary files differ
diff --git a/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png
new file mode 100644
index 00000000000..47719113805
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_template_dropdown_mit_license.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_upload_file_dialog.png b/doc/user/project/repository/img/web_editor_upload_file_dialog.png
index 9d6d8250bbe..9d6d8250bbe 100644
--- a/doc/workflow/img/web_editor_upload_file_dialog.png
+++ b/doc/user/project/repository/img/web_editor_upload_file_dialog.png
Binary files differ
diff --git a/doc/workflow/img/web_editor_upload_file_dropdown.png b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png
index 6b5205b05ec..6b5205b05ec 100644
--- a/doc/workflow/img/web_editor_upload_file_dropdown.png
+++ b/doc/user/project/repository/img/web_editor_upload_file_dropdown.png
Binary files differ
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
new file mode 100644
index 00000000000..7c041d019bb
--- /dev/null
+++ b/doc/user/project/repository/web_editor.md
@@ -0,0 +1,175 @@
+# GitLab Web Editor
+
+Sometimes it's easier to make quick changes directly from the GitLab interface
+than to clone the project and use the Git command line tool. In this feature
+highlight we look at how you can create a new file, directory, branch or
+tag from the file browser. All of these actions are available from a single
+dropdown menu.
+
+## Create a file
+
+From a project's files page, click the '+' button to the right of the branch selector.
+Choose **New file** from the dropdown.
+
+![New file dropdown menu](img/web_editor_new_file_dropdown.png)
+
+---
+
+Enter a file name in the **File name** box. Then, add file content in the editor
+area. Add a descriptive commit message and choose a branch. The branch field
+will default to the branch you were viewing in the file browser. If you enter
+a new branch name, a checkbox will appear allowing you to start a new merge
+request after you commit the changes.
+
+When you are satisfied with your new file, click **Commit Changes** at the bottom.
+
+![Create file editor](img/web_editor_new_file_editor.png)
+
+### Template dropdowns
+
+When starting a new project, there are some common files which the new project
+might need too. Therefore a message will be displayed by GitLab to make this
+easy for you.
+
+![First file for your project](img/web_editor_template_dropdown_first_file.png)
+
+When clicking on either `LICENSE` or `.gitignore`, a dropdown will be displayed
+to provide you with a template which might be suitable for your project.
+
+![MIT license selected](img/web_editor_template_dropdown_mit_license.png)
+
+The license, changelog, contribution guide, or `.gitlab-ci.yml` file could also
+be added through a button on the project page. In the example below the license
+has already been created, which creates a link to the license itself.
+
+![New file button](img/web_editor_template_dropdown_buttons.png)
+
+>**Note:**
+The **Set up CI** button will not appear on an empty repository. You have to at
+least add a file in order for the button to show up.
+
+## Upload a file
+
+The ability to create a file is great when the content is text. However, this
+doesn't work well for binary data such as images, PDFs or other file types. In
+this case you need to upload a file.
+
+From a project's files page, click the '+' button to the right of the branch
+selector. Choose **Upload file** from the dropdown.
+
+![Upload file dropdown menu](img/web_editor_upload_file_dropdown.png)
+
+---
+
+Once the upload dialog pops up there are two ways to upload your file. Either
+drag and drop a file on the pop up or use the **click to upload** link. A file
+preview will appear once you have selected a file to upload.
+
+Enter a commit message, choose a branch, and click **Upload file** when you are
+ready.
+
+![Upload file dialog](img/web_editor_upload_file_dialog.png)
+
+## Create a directory
+
+To keep files in the repository organized it is often helpful to create a new
+directory.
+
+From a project's files page, click the '+' button to the right of the branch selector.
+Choose **New directory** from the dropdown.
+
+![New directory dropdown](img/web_editor_new_directory_dropdown.png)
+
+---
+
+In the new directory dialog enter a directory name, a commit message and choose
+the target branch. Click **Create directory** to finish.
+
+![New directory dialog](img/web_editor_new_directory_dialog.png)
+
+## Create a new branch
+
+There are multiple ways to create a branch from GitLab's web interface.
+
+### Create a new branch from an issue
+
+> [Introduced][ce-2808] in GitLab 8.6.
+
+In case your development workflow dictates to have an issue for every merge
+request, you can quickly create a branch right on the issue page which will be
+tied with the issue itself. You can see a **New Branch** button after the issue
+description, unless there is already a branch with the same name or a referenced
+merge request.
+
+![New Branch Button](img/new_branch_from_issue.png)
+
+Once you click it, a new branch will be created that diverges from the default
+branch of your project, by default `master`. The branch name will be based on
+the title of the issue and as suffix it will have its ID. Thus, the example
+screenshot above will yield a branch named
+`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
+
+After the branch is created, you can edit files in the repository to fix
+the issue. When a merge request is created based on the newly created branch,
+the description field will automatically display the [issue closing pattern]
+`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
+merge request is merged.
+
+### Create a new branch from a project's dashboard
+
+If you want to make changes to several files before creating a new merge
+request, you can create a new branch up front. From a project's files page,
+choose **New branch** from the dropdown.
+
+![New branch dropdown](img/web_editor_new_branch_dropdown.png)
+
+---
+
+Enter a new **Branch name**. Optionally, change the **Create from** field
+to choose which branch, tag or commit SHA this new branch will originate from.
+This field will autocomplete if you start typing an existing branch or tag.
+Click **Create branch** and you will be returned to the file browser on this new
+branch.
+
+![New branch page](img/web_editor_new_branch_page.png)
+
+---
+
+You can now make changes to any files, as needed. When you're ready to merge
+the changes back to master you can use the widget at the top of the screen.
+This widget only appears for a period of time after you create the branch or
+modify files.
+
+![New push widget](img/web_editor_new_push_widget.png)
+
+## Create a new tag
+
+Tags are useful for marking major milestones such as production releases,
+release candidates, and more. You can create a tag from a branch or a commit
+SHA. From a project's files page, choose **New tag** from the dropdown.
+
+![New tag dropdown](img/web_editor_new_tag_dropdown.png)
+
+---
+
+Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you
+would like to create this new tag. You can optionally add a message and
+release notes. The release notes section supports markdown format and you can
+also upload an attachment. Click **Create tag** and you will be taken to the tag
+list page.
+
+![New tag page](img/web_editor_new_tag_page.png)
+
+## Tips
+
+When creating or uploading a new file, or creating a new directory, you can
+trigger a new merge request rather than committing directly to master. Enter
+a new branch name in the **Target branch** field. You will notice a checkbox
+appear that is labeled **Start a new merge request with these changes**. After
+you commit the changes you will be taken to a new merge request form.
+
+![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
+
+![New file button](basicsimages/file_button.png)
+[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
+[issue closing pattern]: ../customization/issue_closing.md
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 0cf56449de2..dcb9c32ad58 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -1,6 +1,5 @@
# Workflow
-- [Authorization for merge requests](authorization_for_merge_requests.md)
- [Change your time zone](timezone.md)
- [Description templates](../user/project/description_templates.md)
- [Feature branch workflow](workflow.md)
@@ -18,14 +17,18 @@
- [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)
+- [Web Editor](../user/project/repository/web_editor.md)
- [Releases](releases.md)
- [Milestones](milestones.md)
-- [Merge Requests](merge_requests.md)
-- [Revert changes](revert_changes.md)
-- [Cherry-pick changes](cherry_pick_changes.md)
-- ["Work In Progress" Merge Requests](wip_merge_requests.md)
-- [Merge When Build Succeeds](merge_when_build_succeeds.md)
+- [Merge Requests](../user/project/merge_requests.md)
+ - [Authorization for merge requests](../user/project/merge_requests/authorization_for_merge_requests.md)
+ - [Cherry-pick changes](../user/project/merge_requests/cherry_pick_changes.md)
+ - [Merge when build succeeds](../user/project/merge_requests/merge_when_build_succeeds.md)
+ - [Resolve discussion comments in merge requests reviews](../user/project/merge_requests/merge_request_discussion_resolution.md)
+ - [Resolve merge conflicts in the UI](../user/project/merge_requests/resolve_conflicts.md)
+ - [Revert changes in the UI](../user/project/merge_requests/revert_changes.md)
+ - [Merge requests versions](../user/project/merge_requests/versions.md)
+ - ["Work In Progress" merge requests](../user/project/merge_requests/work_in_progress_merge_requests.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
- [Importing from SVN, GitHub, BitBucket, etc](importing/README.md)
- [Todos](todos.md)
diff --git a/doc/workflow/authorization_for_merge_requests.md b/doc/workflow/authorization_for_merge_requests.md
index d1d6d94ec11..7bf80a3ad0d 100644
--- a/doc/workflow/authorization_for_merge_requests.md
+++ b/doc/workflow/authorization_for_merge_requests.md
@@ -1,40 +1 @@
-# Authorization for Merge requests
-
-There are two main ways to have a merge request flow with GitLab: working with protected branches in a single repository, or working with forks of an authoritative project.
-
-## Protected branch flow
-
-With the protected branch flow everybody works within the same GitLab project.
-
-The project maintainers get Master access and the regular developers get Developer access.
-
-The maintainers mark the authoritative branches as 'Protected'.
-
-The developers push feature branches to the project and create merge requests to have their feature branches reviewed and merged into one of the protected branches.
-
-Only users with Master access can merge changes into a protected branch.
-
-### Advantages
-
-- fewer projects means less clutter
-- developers need to consider only one remote repository
-
-### Disadvantages
-
-- manual setup of protected branch required for each new project
-
-## Forking workflow
-
-With the forking workflow the maintainers get Master access and the regular developers get Reporter access to the authoritative repository, which prohibits them from pushing any changes to it.
-
-Developers create forks of the authoritative project and push their feature branches to their own forks.
-
-To get their changes into master they need to create a merge request across forks.
-
-### Advantages
-
-- in an appropriately configured GitLab group, new projects automatically get the required access restrictions for regular developers: fewer manual steps to configure authorization for new projects
-
-### Disadvantages
-
-- the project need to keep their forks up to date, which requires more advanced Git skills (managing multiple remotes)
+This document was moved to [user/project/merge_requests/authorization_for_merge_requests](../user/project/merge_requests/authorization_for_merge_requests.md)
diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md
index 64b94d81024..663ffd3f746 100644
--- a/doc/workflow/cherry_pick_changes.md
+++ b/doc/workflow/cherry_pick_changes.md
@@ -1,52 +1 @@
-# Cherry-pick changes
-
-> [Introduced][ce-3514] in GitLab 8.7.
-
----
-
-GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
-with introducing a **Cherry-pick** button in Merge Requests and commit details.
-
-## Cherry-picking a Merge Request
-
-After the Merge Request has been merged, a **Cherry-pick** button will be available
-to cherry-pick the changes introduced by that Merge Request:
-
-![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
-
----
-
-You can cherry-pick the changes directly into the selected branch or you can opt to
-create a new Merge Request with the cherry-pick changes:
-
-![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
-
-## Cherry-picking a Commit
-
-You can cherry-pick a Commit from the Commit details page:
-
-![Cherry-pick commit](img/cherry_pick_changes_commit.png)
-
----
-
-Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
-directly into the target branch or create a new Merge Request to cherry-pick the
-changes:
-
-![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
-
----
-
-Please note that when cherry-picking merge commits, the mainline will always be the
-first parent. If you want to use a different mainline then you need to do that
-from the command line.
-
-Here is a quick example to cherry-pick a merge commit using the second parent as the
-mainline:
-
-```bash
-git cherry-pick -m 2 7a39eb0
-```
-
-[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
-[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
+This document was moved to [user/project/merge_requests/cherry_pick_changes](../user/project/merge_requests/cherry_pick_changes.md).
diff --git a/doc/workflow/merge_requests.md b/doc/workflow/merge_requests.md
index 40a5e4476be..a68bb8b27ca 100644
--- a/doc/workflow/merge_requests.md
+++ b/doc/workflow/merge_requests.md
@@ -1,90 +1 @@
-# Merge Requests
-
-Merge requests allow you to exchange changes you made to source code
-
-## Only allow merge requests to be merged if the build succeeds
-
-You can prevent merge requests from being merged if their build did not succeed
-in the project settings page.
-
-![only_allow_merge_if_build_succeeds](merge_requests/only_allow_merge_if_build_succeeds.png)
-
-Navigate to project settings page and select the `Only allow merge requests to be merged if the build succeeds` check box.
-
-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:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-ce.git
- fetch = +refs/heads/*:refs/remotes/origin/*
-```
-
-Now add the line `fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*` to this section.
-
-It should look like this:
-
-```
-[remote "origin"]
- url = https://gitlab.com/gitlab-org/gitlab-ce.git
- fetch = +refs/heads/*:refs/remotes/origin/*
- fetch = +refs/merge-requests/*/head:refs/remotes/origin/merge-requests/*
-```
-
-Now you can fetch all the merge requests:
-
-```
-$ git fetch origin
-From https://gitlab.com/gitlab-org/gitlab-ce.git
- * [new ref] refs/merge-requests/1/head -> origin/merge-requests/1
- * [new ref] refs/merge-requests/2/head -> origin/merge-requests/2
-...
-```
-
-To check out a particular merge request:
-
-```
-$ git checkout origin/merge-requests/1
-```
-
-## Ignore whitespace changes in Merge Request diff view
-
-![MR diff](merge_requests/merge_request_diff.png)
-
-If you click the "Hide whitespace changes" button, you can see the diff without whitespace changes.
-
-![MR diff without whitespace](merge_requests/merge_request_diff_without_whitespace.png)
-
-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)
+This document was moved to [user/project/merge_requests](../user/project/merge_requests.md).
diff --git a/doc/workflow/merge_requests/merge_request_diff.png b/doc/workflow/merge_requests/merge_request_diff.png
deleted file mode 100644
index 3ebbfb75ea3..00000000000
--- a/doc/workflow/merge_requests/merge_request_diff.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png b/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
deleted file mode 100644
index a0db535019c..00000000000
--- a/doc/workflow/merge_requests/merge_request_diff_without_whitespace.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/merge_requests/versions.png b/doc/workflow/merge_requests/versions.png
deleted file mode 100644
index c0a6dfe6806..00000000000
--- a/doc/workflow/merge_requests/versions.png
+++ /dev/null
Binary files differ
diff --git a/doc/workflow/merge_when_build_succeeds.md b/doc/workflow/merge_when_build_succeeds.md
index 75e1fdff2b2..95afd12ebdb 100644
--- a/doc/workflow/merge_when_build_succeeds.md
+++ b/doc/workflow/merge_when_build_succeeds.md
@@ -1,15 +1 @@
-# Merge When Build Succeeds
-
-When reviewing a merge request that looks ready to merge but still has one or more CI builds running, you can set it to be merged automatically when all builds succeed. This way, you don't have to wait for the builds to finish and remember to merge the request manually.
-
-![Enable](merge_when_build_succeeds/enable.png)
-
-When you hit the "Merge When Build Succeeds" button, the status of the merge request will be updated to represent the impending merge. If you cannot wait for the build to succeed and want to merge immediately, this option is available in the dropdown menu on the right of the main button.
-
-Both team developers and the author of the merge request have the option to cancel the automatic merge if they find a reason why it shouldn't be merged after all.
-
-![Status](merge_when_build_succeeds/status.png)
-
-When the build succeeds, the merge request will automatically be merged. When the build fails, the author gets a chance to retry any failed builds, or to push new commits to fix the failure.
-
-When the builds are retried and succeed on the second try, the merge request will automatically be merged after all. When the merge request is updated with new commits, the automatic merge is automatically canceled to allow the new changes to be reviewed.
+This document was moved to [user/project/merge_requests/merge_when_build_succeeds](../user/project/merge_requests/merge_when_build_succeeds.md).
diff --git a/doc/workflow/revert_changes.md b/doc/workflow/revert_changes.md
index 5ead9f4177f..cf1292253fc 100644
--- a/doc/workflow/revert_changes.md
+++ b/doc/workflow/revert_changes.md
@@ -1,64 +1 @@
-# Reverting changes
-
-> [Introduced][ce-1990] in GitLab 8.5.
-
----
-
-GitLab implements Git's powerful feature to [revert any commit][git-revert]
-with introducing a **Revert** button in Merge Requests and commit details.
-
-## Reverting a Merge Request
-
-_**Note:** The **Revert** button will only be available for Merge Requests
-created since GitLab 8.5. However, you can still revert a Merge Request
-by reverting the merge commit from the list of Commits page._
-
-After the Merge Request has been merged, a **Revert** button will be available
-to revert the changes introduced by that Merge Request:
-
-![Revert Merge Request](img/revert_changes_mr.png)
-
----
-
-You can revert the changes directly into the selected branch or you can opt to
-create a new Merge Request with the revert changes:
-
-![Revert Merge Request modal](img/revert_changes_mr_modal.png)
-
----
-
-After the Merge Request has been reverted, the **Revert** button will not be
-available anymore.
-
-## Reverting a Commit
-
-You can revert a Commit from the Commit details page:
-
-![Revert commit](img/revert_changes_commit.png)
-
----
-
-Similar to reverting a Merge Request, you can opt to revert the changes
-directly into the target branch or create a new Merge Request to revert the
-changes:
-
-![Revert commit modal](img/revert_changes_commit_modal.png)
-
----
-
-After the Commit has been reverted, the **Revert** button will not be available
-anymore.
-
-Please note that when reverting merge commits, the mainline will always be the
-first parent. If you want to use a different mainline then you need to do that
-from the command line.
-
-Here is a quick example to revert a merge commit using the second parent as the
-mainline:
-
-```bash
-git revert -m 2 7a39eb0
-```
-
-[ce-1990]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1990 "Revert button Merge Request"
-[git-revert]: https://git-scm.com/docs/git-revert "Git revert documentation"
+This document was moved to [user/project/merge_requests/revert_changes](../user/project/merge_requests/revert_changes.md).
diff --git a/doc/workflow/web_editor.md b/doc/workflow/web_editor.md
index ee8e7862572..595c7da155b 100644
--- a/doc/workflow/web_editor.md
+++ b/doc/workflow/web_editor.md
@@ -1,151 +1 @@
-# GitLab Web Editor
-
-Sometimes it's easier to make quick changes directly from the GitLab interface
-than to clone the project and use the Git command line tool. In this feature
-highlight we look at how you can create a new file, directory, branch or
-tag from the file browser. All of these actions are available from a single
-dropdown menu.
-
-## Create a file
-
-From a project's files page, click the '+' button to the right of the branch selector.
-Choose **New file** from the dropdown.
-
-![New file dropdown menu](img/web_editor_new_file_dropdown.png)
-
----
-
-Enter a file name in the **File name** box. Then, add file content in the editor
-area. Add a descriptive commit message and choose a branch. The branch field
-will default to the branch you were viewing in the file browser. If you enter
-a new branch name, a checkbox will appear allowing you to start a new merge
-request after you commit the changes.
-
-When you are satisfied with your new file, click **Commit Changes** at the bottom.
-
-![Create file editor](img/web_editor_new_file_editor.png)
-
-## Upload a file
-
-The ability to create a file is great when the content is text. However, this
-doesn't work well for binary data such as images, PDFs or other file types. In
-this case you need to upload a file.
-
-From a project's files page, click the '+' button to the right of the branch
-selector. Choose **Upload file** from the dropdown.
-
-![Upload file dropdown menu](img/web_editor_upload_file_dropdown.png)
-
----
-
-Once the upload dialog pops up there are two ways to upload your file. Either
-drag and drop a file on the pop up or use the **click to upload** link. A file
-preview will appear once you have selected a file to upload.
-
-Enter a commit message, choose a branch, and click **Upload file** when you are
-ready.
-
-![Upload file dialog](img/web_editor_upload_file_dialog.png)
-
-## Create a directory
-
-To keep files in the repository organized it is often helpful to create a new
-directory.
-
-From a project's files page, click the '+' button to the right of the branch selector.
-Choose **New directory** from the dropdown.
-
-![New directory dropdown](img/web_editor_new_directory_dropdown.png)
-
----
-
-In the new directory dialog enter a directory name, a commit message and choose
-the target branch. Click **Create directory** to finish.
-
-![New directory dialog](img/web_editor_new_directory_dialog.png)
-
-## Create a new branch
-
-There are multiple ways to create a branch from GitLab's web interface.
-
-### Create a new branch from an issue
-
-> [Introduced][ce-2808] in GitLab 8.6.
-
-In case your development workflow dictates to have an issue for every merge
-request, you can quickly create a branch right on the issue page which will be
-tied with the issue itself. You can see a **New Branch** button after the issue
-description, unless there is already a branch with the same name or a referenced
-merge request.
-
-![New Branch Button](img/new_branch_from_issue.png)
-
-Once you click it, a new branch will be created that diverges from the default
-branch of your project, by default `master`. The branch name will be based on
-the title of the issue and as suffix it will have its ID. Thus, the example
-screenshot above will yield a branch named
-`2-et-cum-et-sed-expedita-repellat-consequatur-ut-assumenda-numquam-rerum`.
-
-After the branch is created, you can edit files in the repository to fix
-the issue. When a merge request is created based on the newly created branch,
-the description field will automatically display the [issue closing pattern]
-`Closes #ID`, where `ID` the ID of the issue. This will close the issue once the
-merge request is merged.
-
-### Create a new branch from a project's dashboard
-
-If you want to make changes to several files before creating a new merge
-request, you can create a new branch up front. From a project's files page,
-choose **New branch** from the dropdown.
-
-![New branch dropdown](img/web_editor_new_branch_dropdown.png)
-
----
-
-Enter a new **Branch name**. Optionally, change the **Create from** field
-to choose which branch, tag or commit SHA this new branch will originate from.
-This field will autocomplete if you start typing an existing branch or tag.
-Click **Create branch** and you will be returned to the file browser on this new
-branch.
-
-![New branch page](img/web_editor_new_branch_page.png)
-
----
-
-You can now make changes to any files, as needed. When you're ready to merge
-the changes back to master you can use the widget at the top of the screen.
-This widget only appears for a period of time after you create the branch or
-modify files.
-
-![New push widget](img/web_editor_new_push_widget.png)
-
-## Create a new tag
-
-Tags are useful for marking major milestones such as production releases,
-release candidates, and more. You can create a tag from a branch or a commit
-SHA. From a project's files page, choose **New tag** from the dropdown.
-
-![New tag dropdown](img/web_editor_new_tag_dropdown.png)
-
----
-
-Give the tag a name such as `v1.0.0`. Choose the branch or SHA from which you
-would like to create this new tag. You can optionally add a message and
-release notes. The release notes section supports markdown format and you can
-also upload an attachment. Click **Create tag** and you will be taken to the tag
-list page.
-
-![New tag page](img/web_editor_new_tag_page.png)
-
-## Tips
-
-When creating or uploading a new file, or creating a new directory, you can
-trigger a new merge request rather than committing directly to master. Enter
-a new branch name in the **Target branch** field. You will notice a checkbox
-appear that is labeled **Start a new merge request with these changes**. After
-you commit the changes you will be taken to a new merge request form.
-
-![Start a new merge request with these changes](img/web_editor_start_new_merge_request.png)
-
-[ce-2808]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2808
-[issue closing pattern]: ../customization/issue_closing.md
+This document was moved to [user/project/repository/web_editor](../user/project/repository/web_editor.md).
diff --git a/doc/workflow/wip_merge_requests.md b/doc/workflow/wip_merge_requests.md
index 46035a5e6b6..abb8002f442 100644
--- a/doc/workflow/wip_merge_requests.md
+++ b/doc/workflow/wip_merge_requests.md
@@ -1,13 +1 @@
-# "Work In Progress" Merge Requests
-
-To prevent merge requests from accidentally being accepted before they're completely ready, GitLab blocks the "Accept" button for merge requests that have been marked a **Work In Progress**.
-
-![Blocked Accept Button](wip_merge_requests/blocked_accept_button.png)
-
-To mark a merge request a Work In Progress, simply start its title with `[WIP]` or `WIP:`.
-
-![Mark as WIP](wip_merge_requests/mark_as_wip.png)
-
-To allow a Work In Progress merge request to be accepted again when it's ready, simply remove the `WIP` prefix.
-
-![Unark as WIP](wip_merge_requests/unmark_as_wip.png)
+This document was moved to [user/project/merge_requests/work_in_progress_merge_requests](../user/project/merge_requests/work_in_progress_merge_requests.md).
diff --git a/features/steps/admin/settings.rb b/features/steps/admin/settings.rb
index 03f87df7a60..11dc7f580f0 100644
--- a/features/steps/admin/settings.rb
+++ b/features/steps/admin/settings.rb
@@ -33,6 +33,7 @@ class Spinach::Features::AdminSettings < Spinach::FeatureSteps
page.check('Issue')
page.check('Merge request')
page.check('Build')
+ page.check('Pipeline')
click_on 'Save'
end
diff --git a/features/steps/project/merge_requests.rb b/features/steps/project/merge_requests.rb
index 56b28949585..df17b5626c6 100644
--- a/features/steps/project/merge_requests.rb
+++ b/features/steps/project/merge_requests.rb
@@ -31,7 +31,9 @@ class Spinach::Features::ProjectMergeRequests < Spinach::FeatureSteps
end
step 'I click link "Closed"' do
- click_link "Closed"
+ page.within('.issues-state-filters') do
+ click_link "Closed"
+ end
end
step 'I should see merge request "Wiki Feature"' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index e14464c1b0d..a08fb056049 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -45,6 +45,7 @@ module API
mount ::API::Keys
mount ::API::Labels
mount ::API::LicenseTemplates
+ mount ::API::Lint
mount ::API::Members
mount ::API::MergeRequests
mount ::API::Milestones
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 5b54c11ef62..6e6efece7c4 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -105,15 +105,19 @@ module API
post '/two_factor_recovery_codes' do
status 200
- key = Key.find(params[:key_id])
- user = key.user
+ key = Key.find_by(id: params[:key_id])
+
+ unless key
+ return { 'success' => false, 'message' => 'Could not find the given key' }
+ end
- # Make sure this isn't a deploy key
- unless key.type.nil?
+ if key.is_a?(DeployKey)
return { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
end
- unless user.present?
+ user = key.user
+
+ unless user
return { success: false, message: 'Could not find a user for the given key' }
end
diff --git a/lib/api/lint.rb b/lib/api/lint.rb
new file mode 100644
index 00000000000..ae43a4a3237
--- /dev/null
+++ b/lib/api/lint.rb
@@ -0,0 +1,21 @@
+module API
+ class Lint < Grape::API
+ namespace :ci do
+ desc 'Validation of .gitlab-ci.yml content'
+ params do
+ requires :content, type: String, desc: 'Content of .gitlab-ci.yml'
+ end
+ post '/lint' do
+ error = Ci::GitlabCiYamlProcessor.validation_message(params[:content])
+
+ status 200
+
+ if error.blank?
+ { status: 'valid', errors: [] }
+ else
+ { status: 'invalid', errors: [error] }
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/pipelines.rb b/lib/api/pipelines.rb
index 2aae75c471d..2a0c8e1f2c0 100644
--- a/lib/api/pipelines.rb
+++ b/lib/api/pipelines.rb
@@ -13,11 +13,14 @@ module API
params do
optional :page, type: Integer, desc: 'Page number of the current request'
optional :per_page, type: Integer, desc: 'Number of items per page'
+ optional :scope, type: String, values: ['running', 'branches', 'tags'],
+ desc: 'Either running, branches, or tags'
end
get ':id/pipelines' do
authorize! :read_pipeline, user_project
- present paginate(user_project.pipelines), with: Entities::Pipeline
+ pipelines = PipelinesFinder.new(user_project).execute(scope: params[:scope])
+ present paginate(pipelines), with: Entities::Pipeline
end
desc 'Gets a specific pipeline for the project' do
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index a1fd598414a..4033f597859 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -189,16 +189,30 @@ module API
end
end
- # Fork new project for the current user.
+ # Fork new project for the current user or provided namespace.
#
# Parameters:
# id (required) - The ID of a project
+ # namespace (optional) - The ID or name of the namespace that the project will be forked into.
# Example Request
# POST /projects/fork/:id
post 'fork/:id' do
+ attrs = {}
+ namespace_id = params[:namespace]
+
+ if namespace_id.present?
+ namespace = Namespace.find_by(id: namespace_id) || Namespace.find_by_path_or_name(namespace_id)
+
+ not_found!('Target Namespace') unless namespace
+
+ attrs[:namespace] = namespace
+ end
+
@forked_project =
::Projects::ForkService.new(user_project,
- current_user).execute
+ current_user,
+ attrs).execute
+
if @forked_project.errors.any?
conflict!(@forked_project.errors.messages)
else
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 9f3b582a263..54db63d4628 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -12,7 +12,7 @@ module Ci
# POST /builds/register
post "register" do
authenticate_runner!
- update_runner_last_contact
+ update_runner_last_contact(save: false)
update_runner_info
required_attributes! [:token]
not_found! unless current_runner.active?
@@ -101,6 +101,7 @@ module Ci
# POST /builds/:id/artifacts/authorize
post ":id/artifacts/authorize" do
require_gitlab_workhorse!
+ Gitlab::Workhorse.verify_api_request!(headers)
not_allowed! unless Gitlab.config.artifacts.enabled
build = Ci::Build.find_by_id(params[:id])
not_found! unless build
@@ -113,7 +114,8 @@ module Ci
end
status 200
- { TempPath: ArtifactUploader.artifacts_upload_path }
+ content_type Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
+ Gitlab::Workhorse.artifact_upload_ok
end
# Upload artifacts to build - Runners only
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 199d62d9b8a..bcabf7a21b2 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -3,7 +3,7 @@ module Ci
module Helpers
BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
BUILD_TOKEN_PARAM = :token
- UPDATE_RUNNER_EVERY = 60
+ UPDATE_RUNNER_EVERY = 40 * 60
def authenticate_runners!
forbidden! unless runner_registration_token_valid?
@@ -22,11 +22,13 @@ module Ci
params[:token] == current_application_settings.runners_registration_token
end
- def update_runner_last_contact
+ def update_runner_last_contact(save: true)
# Use a random threshold to prevent beating DB updates
+ # it generates a distribution between: [40m, 80m]
contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age
- current_runner.update_attributes(contacted_at: Time.now)
+ current_runner.contacted_at = Time.now
+ current_runner.save if current_runner.changed? && save
end
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 47efd5bd9f2..caa815f720f 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -55,12 +55,7 @@ module Ci
{
stage_idx: @stages.index(job[:stage]),
stage: job[:stage],
- ##
- # Refactoring note:
- # - before script behaves differently than after script
- # - after script returns an array of commands
- # - before script should be a concatenated command
- commands: [job[:before_script] || @before_script, job[:script]].flatten.compact.join("\n"),
+ commands: job[:commands],
tag_list: job[:tags] || [],
name: job[:name].to_s,
allow_failure: job[:allow_failure] || false,
@@ -68,16 +63,27 @@ module Ci
environment: job[:environment],
yaml_variables: yaml_variables(name),
options: {
- image: job[:image] || @image,
- services: job[:services] || @services,
+ image: job[:image],
+ services: job[:services],
artifacts: job[:artifacts],
- cache: job[:cache] || @cache,
+ cache: job[:cache],
dependencies: job[:dependencies],
- after_script: job[:after_script] || @after_script,
+ after_script: job[:after_script],
}.compact
}
end
+ def self.validation_message(content)
+ return 'Please provide content of .gitlab-ci.yml' if content.blank?
+
+ begin
+ Ci::GitlabCiYamlProcessor.new(content)
+ nil
+ rescue ValidationError, Psych::SyntaxError => e
+ e.message
+ end
+ end
+
private
def initial_parsing
diff --git a/lib/gitlab/bitbucket_import/importer.rb b/lib/gitlab/bitbucket_import/importer.rb
index 7beaecd1cf0..f4b5097adb1 100644
--- a/lib/gitlab/bitbucket_import/importer.rb
+++ b/lib/gitlab/bitbucket_import/importer.rb
@@ -21,7 +21,7 @@ module Gitlab
private
- def gl_user_id(project, bitbucket_id)
+ def gitlab_user_id(project, bitbucket_id)
if bitbucket_id
user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'bitbucket'", bitbucket_id.to_s)
(user && user.id) || project.creator_id
@@ -74,7 +74,7 @@ module Gitlab
description: body,
title: issue["title"],
state: %w(resolved invalid duplicate wontfix closed).include?(issue["status"]) ? 'closed' : 'opened',
- author_id: gl_user_id(project, reporter)
+ author_id: gitlab_user_id(project, reporter)
)
end
rescue ActiveRecord::RecordInvalid => e
diff --git a/lib/gitlab/ci/config.rb b/lib/gitlab/ci/config.rb
index ae82c0db3f1..bbfa6cf7d05 100644
--- a/lib/gitlab/ci/config.rb
+++ b/lib/gitlab/ci/config.rb
@@ -14,7 +14,7 @@ module Gitlab
@config = Loader.new(config).load!
@global = Node::Global.new(@config)
- @global.process!
+ @global.compose!
end
def valid?
diff --git a/lib/gitlab/ci/config/node/configurable.rb b/lib/gitlab/ci/config/node/configurable.rb
index 2de82d40c9d..6b7ab2fdaf2 100644
--- a/lib/gitlab/ci/config/node/configurable.rb
+++ b/lib/gitlab/ci/config/node/configurable.rb
@@ -23,9 +23,9 @@ module Gitlab
end
end
- private
+ def compose!(deps = nil)
+ return unless valid?
- def compose!
self.class.nodes.each do |key, factory|
factory
.value(@config[key])
@@ -33,6 +33,12 @@ module Gitlab
@entries[key] = factory.create!
end
+
+ yield if block_given?
+
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
end
class_methods do
diff --git a/lib/gitlab/ci/config/node/entry.rb b/lib/gitlab/ci/config/node/entry.rb
index 0c782c422b5..8717eabf81e 100644
--- a/lib/gitlab/ci/config/node/entry.rb
+++ b/lib/gitlab/ci/config/node/entry.rb
@@ -20,11 +20,14 @@ module Gitlab
@validator.validate(:new)
end
- def process!
+ def [](key)
+ @entries[key] || Node::Undefined.new
+ end
+
+ def compose!(deps = nil)
return unless valid?
- compose!
- descendants.each(&:process!)
+ yield if block_given?
end
def leaf?
@@ -73,11 +76,6 @@ module Gitlab
def self.validator
Validator
end
-
- private
-
- def compose!
- end
end
end
end
diff --git a/lib/gitlab/ci/config/node/factory.rb b/lib/gitlab/ci/config/node/factory.rb
index 707b052e6a8..5387f29ad59 100644
--- a/lib/gitlab/ci/config/node/factory.rb
+++ b/lib/gitlab/ci/config/node/factory.rb
@@ -37,8 +37,8 @@ module Gitlab
# See issue #18775.
#
if @value.nil?
- Node::Undefined.new(
- fabricate_undefined
+ Node::Unspecified.new(
+ fabricate_unspecified
)
else
fabricate(@node, @value)
@@ -47,13 +47,13 @@ module Gitlab
private
- def fabricate_undefined
+ def fabricate_unspecified
##
# If node has a default value we fabricate concrete node
# with default value.
#
if @node.default.nil?
- fabricate(Node::Null)
+ fabricate(Node::Undefined)
else
fabricate(@node, @node.default)
end
diff --git a/lib/gitlab/ci/config/node/global.rb b/lib/gitlab/ci/config/node/global.rb
index ccd539fb003..2a2943c9288 100644
--- a/lib/gitlab/ci/config/node/global.rb
+++ b/lib/gitlab/ci/config/node/global.rb
@@ -36,15 +36,15 @@ module Gitlab
helpers :before_script, :image, :services, :after_script,
:variables, :stages, :types, :cache, :jobs
- private
-
- def compose!
- super
-
- compose_jobs!
- compose_deprecated_entries!
+ def compose!(_deps = nil)
+ super(self) do
+ compose_jobs!
+ compose_deprecated_entries!
+ end
end
+ private
+
def compose_jobs!
factory = Node::Factory.new(Node::Jobs)
.value(@config.except(*self.class.nodes.keys))
diff --git a/lib/gitlab/ci/config/node/job.rb b/lib/gitlab/ci/config/node/job.rb
index e84737acbb9..0cbdf7619c0 100644
--- a/lib/gitlab/ci/config/node/job.rb
+++ b/lib/gitlab/ci/config/node/job.rb
@@ -80,7 +80,19 @@ module Gitlab
helpers :before_script, :script, :stage, :type, :after_script,
:cache, :image, :services, :only, :except, :variables,
- :artifacts
+ :artifacts, :commands
+
+ def compose!(deps = nil)
+ super do
+ if type_defined? && !stage_defined?
+ @entries[:stage] = @entries[:type]
+ end
+
+ @entries.delete(:type)
+ end
+
+ inherit!(deps)
+ end
def name
@metadata[:name]
@@ -90,12 +102,30 @@ module Gitlab
@config.merge(to_hash.compact)
end
+ def commands
+ (before_script_value.to_a + script_value.to_a).join("\n")
+ end
+
private
+ def inherit!(deps)
+ return unless deps
+
+ self.class.nodes.each_key do |key|
+ global_entry = deps[key]
+ job_entry = @entries[key]
+
+ if global_entry.specified? && !job_entry.specified?
+ @entries[key] = global_entry
+ end
+ end
+ end
+
def to_hash
{ name: name,
before_script: before_script,
script: script,
+ commands: commands,
image: image,
services: services,
stage: stage,
@@ -106,16 +136,6 @@ module Gitlab
artifacts: artifacts,
after_script: after_script }
end
-
- def compose!
- super
-
- if type_defined? && !stage_defined?
- @entries[:stage] = @entries[:type]
- end
-
- @entries.delete(:type)
- end
end
end
end
diff --git a/lib/gitlab/ci/config/node/jobs.rb b/lib/gitlab/ci/config/node/jobs.rb
index a1a26d4fd8f..d10e80d1a7d 100644
--- a/lib/gitlab/ci/config/node/jobs.rb
+++ b/lib/gitlab/ci/config/node/jobs.rb
@@ -26,19 +26,23 @@ module Gitlab
name.to_s.start_with?('.')
end
- private
-
- def compose!
- @config.each do |name, config|
- node = hidden?(name) ? Node::Hidden : Node::Job
-
- factory = Node::Factory.new(node)
- .value(config || {})
- .metadata(name: name)
- .with(key: name, parent: self,
- description: "#{name} job definition.")
+ def compose!(deps = nil)
+ super do
+ @config.each do |name, config|
+ node = hidden?(name) ? Node::Hidden : Node::Job
+
+ factory = Node::Factory.new(node)
+ .value(config || {})
+ .metadata(name: name)
+ .with(key: name, parent: self,
+ description: "#{name} job definition.")
+
+ @entries[name] = factory.create!
+ end
- @entries[name] = factory.create!
+ @entries.each_value do |entry|
+ entry.compose!(deps)
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/node/null.rb b/lib/gitlab/ci/config/node/null.rb
deleted file mode 100644
index 88a5f53f13c..00000000000
--- a/lib/gitlab/ci/config/node/null.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-module Gitlab
- module Ci
- class Config
- module Node
- ##
- # This class represents an undefined node.
- #
- # Implements the Null Object pattern.
- #
- class Null < Entry
- def value
- nil
- end
-
- def valid?
- true
- end
-
- def errors
- []
- end
-
- def specified?
- false
- end
-
- def relevant?
- false
- end
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ci/config/node/undefined.rb b/lib/gitlab/ci/config/node/undefined.rb
index 45fef8c3ae5..33e78023539 100644
--- a/lib/gitlab/ci/config/node/undefined.rb
+++ b/lib/gitlab/ci/config/node/undefined.rb
@@ -3,15 +3,34 @@ module Gitlab
class Config
module Node
##
- # This class represents an unspecified entry node.
+ # This class represents an undefined node.
#
- # It decorates original entry adding method that indicates it is
- # unspecified.
+ # Implements the Null Object pattern.
#
- class Undefined < SimpleDelegator
+ class Undefined < Entry
+ def initialize(*)
+ super(nil)
+ end
+
+ def value
+ nil
+ end
+
+ def valid?
+ true
+ end
+
+ def errors
+ []
+ end
+
def specified?
false
end
+
+ def relevant?
+ false
+ end
end
end
end
diff --git a/lib/gitlab/ci/config/node/unspecified.rb b/lib/gitlab/ci/config/node/unspecified.rb
new file mode 100644
index 00000000000..a7d1f6131b8
--- /dev/null
+++ b/lib/gitlab/ci/config/node/unspecified.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Ci
+ class Config
+ module Node
+ ##
+ # This class represents an unspecified entry node.
+ #
+ # It decorates original entry adding method that indicates it is
+ # unspecified.
+ #
+ class Unspecified < SimpleDelegator
+ def specified?
+ false
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline_duration.rb b/lib/gitlab/ci/pipeline_duration.rb
new file mode 100644
index 00000000000..a210e76acaa
--- /dev/null
+++ b/lib/gitlab/ci/pipeline_duration.rb
@@ -0,0 +1,141 @@
+module Gitlab
+ module Ci
+ # # Introduction - total running time
+ #
+ # The problem this module is trying to solve is finding the total running
+ # time amongst all the jobs, excluding retries and pending (queue) time.
+ # We could reduce this problem down to finding the union of periods.
+ #
+ # So each job would be represented as a `Period`, which consists of
+ # `Period#first` as when the job started and `Period#last` as when the
+ # job was finished. A simple example here would be:
+ #
+ # * A (1, 3)
+ # * B (2, 4)
+ # * C (6, 7)
+ #
+ # Here A begins from 1, and ends to 3. B begins from 2, and ends to 4.
+ # C begins from 6, and ends to 7. Visually it could be viewed as:
+ #
+ # 0 1 2 3 4 5 6 7
+ # AAAAAAA
+ # BBBBBBB
+ # CCCC
+ #
+ # The union of A, B, and C would be (1, 4) and (6, 7), therefore the
+ # total running time should be:
+ #
+ # (4 - 1) + (7 - 6) => 4
+ #
+ # # The Algorithm
+ #
+ # The algorithm used here for union would be described as follow.
+ # First we make sure that all periods are sorted by `Period#first`.
+ # Then we try to merge periods by iterating through the first period
+ # to the last period. The goal would be merging all overlapped periods
+ # so that in the end all the periods are discrete. When all periods
+ # are discrete, we're free to just sum all the periods to get real
+ # running time.
+ #
+ # Here we begin from A, and compare it to B. We could find that
+ # before A ends, B already started. That is `B.first <= A.last`
+ # that is `2 <= 3` which means A and B are overlapping!
+ #
+ # When we found that two periods are overlapping, we would need to merge
+ # them into a new period and disregard the old periods. To make a new
+ # period, we take `A.first` as the new first because remember? we sorted
+ # them, so `A.first` must be smaller or equal to `B.first`. And we take
+ # `[A.last, B.last].max` as the new last because we want whoever ended
+ # later. This could be broken into two cases:
+ #
+ # 0 1 2 3 4
+ # AAAAAAA
+ # BBBBBBB
+ #
+ # Or:
+ #
+ # 0 1 2 3 4
+ # AAAAAAAAAA
+ # BBBB
+ #
+ # So that we need to take whoever ends later. Back to our example,
+ # after merging and discard A and B it could be visually viewed as:
+ #
+ # 0 1 2 3 4 5 6 7
+ # DDDDDDDDDD
+ # CCCC
+ #
+ # Now we could go on and compare the newly created D and the old C.
+ # We could figure out that D and C are not overlapping by checking
+ # `C.first <= D.last` is `false`. Therefore we need to keep both C
+ # and D. The example would end here because there are no more jobs.
+ #
+ # After having the union of all periods, we just need to sum the length
+ # of all periods to get total time.
+ #
+ # (4 - 1) + (7 - 6) => 4
+ #
+ # That is 4 is the answer in the example.
+ module PipelineDuration
+ extend self
+
+ Period = Struct.new(:first, :last) do
+ def duration
+ last - first
+ end
+ end
+
+ def from_pipeline(pipeline)
+ status = %w[success failed running canceled]
+ builds = pipeline.builds.latest.
+ where(status: status).where.not(started_at: nil).order(:started_at)
+
+ from_builds(builds)
+ end
+
+ def from_builds(builds)
+ now = Time.now
+
+ periods = builds.map do |b|
+ Period.new(b.started_at, b.finished_at || now)
+ end
+
+ from_periods(periods)
+ end
+
+ # periods should be sorted by `first`
+ def from_periods(periods)
+ process_duration(process_periods(periods))
+ end
+
+ private
+
+ def process_periods(periods)
+ return periods if periods.empty?
+
+ periods.drop(1).inject([periods.first]) do |result, current|
+ previous = result.last
+
+ if overlap?(previous, current)
+ result[-1] = merge(previous, current)
+ result
+ else
+ result << current
+ end
+ end
+ end
+
+ def overlap?(previous, current)
+ current.first <= previous.last
+ end
+
+ def merge(previous, current)
+ Period.new(previous.first, [previous.last, current.last].max)
+ end
+
+ def process_duration(periods)
+ periods.sum(&:duration)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/conflict/parser.rb b/lib/gitlab/conflict/parser.rb
index 2d4d55daeeb..98e842cded3 100644
--- a/lib/gitlab/conflict/parser.rb
+++ b/lib/gitlab/conflict/parser.rb
@@ -18,7 +18,7 @@ module Gitlab
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
+ raise UnmergeableFile if text.length > 200.kilobytes
begin
text.to_json
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 72992baffd4..d546e102c63 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -15,7 +15,7 @@ module Gitlab
private
- def gl_user_id(github_id)
+ def gitlab_user_id(github_id)
User.joins(:identities).
find_by("identities.extern_uid = ? AND identities.provider = 'github'", github_id.to_s).
try(:id)
diff --git a/lib/gitlab/github_import/comment_formatter.rb b/lib/gitlab/github_import/comment_formatter.rb
index 2c1b94ef2cd..1c7c1a73c77 100644
--- a/lib/gitlab/github_import/comment_formatter.rb
+++ b/lib/gitlab/github_import/comment_formatter.rb
@@ -21,7 +21,7 @@ module Gitlab
end
def author_id
- gl_user_id(raw_data.user.id) || project.creator_id
+ gitlab_user_id(raw_data.user.id) || project.creator_id
end
def body
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 4fdc2f46be0..0388c58f811 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -133,8 +133,7 @@ module Gitlab
if issue.labels.count > 0
label_ids = issue.labels
- .map { |raw| LabelFormatter.new(project, raw).attributes }
- .map { |attrs| Label.find_by(attrs).try(:id) }
+ .map { |attrs| project.labels.find_by(title: attrs.name).try(:id) }
.compact
issuable.update_attribute(:label_ids, label_ids)
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 07edbe37a13..ad4f1d8ae99 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -40,7 +40,7 @@ module Gitlab
def assignee_id
if assigned?
- gl_user_id(raw_data.assignee.id)
+ gitlab_user_id(raw_data.assignee.id)
end
end
@@ -49,7 +49,7 @@ module Gitlab
end
def author_id
- gl_user_id(raw_data.user.id) || project.creator_id
+ gitlab_user_id(raw_data.user.id) || project.creator_id
end
def body
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb
index 9f18244e7d7..2cad7fca88e 100644
--- a/lib/gitlab/github_import/label_formatter.rb
+++ b/lib/gitlab/github_import/label_formatter.rb
@@ -13,6 +13,12 @@ module Gitlab
Label
end
+ def create!
+ project.labels.find_or_create_by!(title: title) do |label|
+ label.color = color
+ end
+ end
+
private
def color
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index d9d436d7490..87e031b27f8 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -68,7 +68,7 @@ module Gitlab
def assignee_id
if assigned?
- gl_user_id(raw_data.assignee.id)
+ gitlab_user_id(raw_data.assignee.id)
end
end
@@ -77,7 +77,7 @@ module Gitlab
end
def author_id
- gl_user_id(raw_data.user.id) || project.creator_id
+ gitlab_user_id(raw_data.user.id) || project.creator_id
end
def body
diff --git a/lib/gitlab/gitlab_import/importer.rb b/lib/gitlab/gitlab_import/importer.rb
index 46d40f75be6..e44d7934fda 100644
--- a/lib/gitlab/gitlab_import/importer.rb
+++ b/lib/gitlab/gitlab_import/importer.rb
@@ -41,7 +41,8 @@ module Gitlab
title: issue["title"],
state: issue["state"],
updated_at: issue["updated_at"],
- author_id: gl_user_id(project, issue["author"]["id"])
+ author_id: gitlab_user_id(project, issue["author"]["id"]),
+ confidential: issue["confidential"]
)
end
end
@@ -51,7 +52,7 @@ module Gitlab
private
- def gl_user_id(project, gitlab_id)
+ def gitlab_user_id(project, gitlab_id)
user = User.joins(:identities).find_by("identities.extern_uid = ? AND identities.provider = 'gitlab'", gitlab_id.to_s)
(user && user.id) || project.creator_id
end
diff --git a/lib/gitlab/popen.rb b/lib/gitlab/popen.rb
index ca23ccef25b..a0fd41161a5 100644
--- a/lib/gitlab/popen.rb
+++ b/lib/gitlab/popen.rb
@@ -21,9 +21,9 @@ module Gitlab
@cmd_output = ""
@cmd_status = 0
Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|
- # We are not using stdin so we should close it, in case the command we
- # are running waits for input.
+ yield(stdin) if block_given?
stdin.close
+
@cmd_output << stdout.read
@cmd_output << stderr.read
@cmd_status = wait_thr.value.exitstatus
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 183bd10d6a3..5b9cfaeb2f8 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -28,11 +28,6 @@ module Gitlab
end
end
- def total_count
- @total_count ||= issues_count + merge_requests_count + blobs_count +
- notes_count + wiki_blobs_count + commits_count
- end
-
def blobs_count
@blobs_count ||= blobs.count
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index f8ab2b1f09e..2690938fe82 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -27,11 +27,6 @@ module Gitlab
end
end
- def total_count
- @total_count ||= projects_count + issues_count + merge_requests_count +
- milestones_count
- end
-
def projects_count
@projects_count ||= projects.count
end
@@ -48,10 +43,6 @@ module Gitlab
@milestones_count ||= milestones.count
end
- def empty?
- total_count.zero?
- end
-
private
def projects
diff --git a/lib/gitlab/snippet_search_results.rb b/lib/gitlab/snippet_search_results.rb
index e0e74ff8359..9e01f02029c 100644
--- a/lib/gitlab/snippet_search_results.rb
+++ b/lib/gitlab/snippet_search_results.rb
@@ -20,10 +20,6 @@ module Gitlab
end
end
- def total_count
- @total_count ||= snippet_titles_count + snippet_blobs_count
- end
-
def snippet_titles_count
@snippet_titles_count ||= snippet_titles.count
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index c6826a09bd2..efe4aeb399d 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -1,19 +1,38 @@
require 'base64'
require 'json'
+require 'securerandom'
module Gitlab
class Workhorse
SEND_DATA_HEADER = 'Gitlab-Workhorse-Send-Data'
VERSION_FILE = 'GITLAB_WORKHORSE_VERSION'
+ INTERNAL_API_CONTENT_TYPE = 'application/vnd.gitlab-workhorse+json'
+ INTERNAL_API_REQUEST_HEADER = 'Gitlab-Workhorse-Api-Request'
+
+ # Supposedly the effective key size for HMAC-SHA256 is 256 bits, i.e. 32
+ # bytes https://tools.ietf.org/html/rfc4868#section-2.6
+ SECRET_LENGTH = 32
class << self
def git_http_ok(repository, user)
{
- 'GL_ID' => Gitlab::GlId.gl_id(user),
- 'RepoPath' => repository.path_to_repo,
+ GL_ID: Gitlab::GlId.gl_id(user),
+ RepoPath: repository.path_to_repo,
}
end
+ def lfs_upload_ok(oid, size)
+ {
+ StoreLFSPath: "#{Gitlab.config.lfs.storage_path}/tmp/upload",
+ LfsOid: oid,
+ LfsSize: size,
+ }
+ end
+
+ def artifact_upload_ok
+ { TempPath: ArtifactUploader.artifacts_upload_path }
+ end
+
def send_git_blob(repository, blob)
params = {
'RepoPath' => repository.path_to_repo,
@@ -81,6 +100,35 @@ module Gitlab
path.readable? ? path.read.chomp : 'unknown'
end
+ def secret
+ @secret ||= begin
+ bytes = Base64.strict_decode64(File.read(secret_path))
+ raise "#{secret_path} does not contain #{SECRET_LENGTH} bytes" if bytes.length != SECRET_LENGTH
+ bytes
+ end
+ end
+
+ def write_secret
+ bytes = SecureRandom.random_bytes(SECRET_LENGTH)
+ File.open(secret_path, 'w:BINARY', 0600) do |f|
+ f.chmod(0600)
+ f.write(Base64.strict_encode64(bytes))
+ end
+ end
+
+ def verify_api_request!(request_headers)
+ JWT.decode(
+ request_headers[INTERNAL_API_REQUEST_HEADER],
+ secret,
+ true,
+ { iss: 'gitlab-workhorse', verify_iss: true, algorithm: 'HS256' },
+ )
+ end
+
+ def secret_path
+ Rails.root.join('.gitlab_workhorse_secret')
+ end
+
protected
def encode(hash)
diff --git a/spec/controllers/import/bitbucket_controller_spec.rb b/spec/controllers/import/bitbucket_controller_spec.rb
index 07bf8d2d1c3..1d3c9fbbe2f 100644
--- a/spec/controllers/import/bitbucket_controller_spec.rb
+++ b/spec/controllers/import/bitbucket_controller_spec.rb
@@ -146,21 +146,42 @@ describe Import::BitbucketController do
end
context "when a namespace with the Bitbucket user's username doesn't exist" do
- it "creates the namespace" do
- expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).and_return(double(execute: true))
+ context "when current user can create namespaces" do
+ it "creates the namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
- post :create, format: :js
+ expect { post :create, format: :js }.to change(Namespace, :count).by(1)
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
+ and_return(double(execute: true))
- expect(Namespace.where(name: other_username).first).not_to be_nil
+ post :create, format: :js
+ end
end
- it "takes the new namespace" do
- expect(Gitlab::BitbucketImport::ProjectCreator).
- to receive(:new).with(bitbucket_repo, an_instance_of(Group), user, access_params).
- and_return(double(execute: true))
+ context "when current user can't create namespaces" do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
- post :create, format: :js
+ it "doesn't create the namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, format: :js }.not_to change(Namespace, :count)
+ end
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::BitbucketImport::ProjectCreator).
+ to receive(:new).with(bitbucket_repo, user.namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
end
end
end
diff --git a/spec/controllers/import/github_controller_spec.rb b/spec/controllers/import/github_controller_spec.rb
index 51d59526854..ebfbf54182b 100644
--- a/spec/controllers/import/github_controller_spec.rb
+++ b/spec/controllers/import/github_controller_spec.rb
@@ -181,21 +181,42 @@ describe Import::GithubController do
end
context "when a namespace with the GitHub user's username doesn't exist" do
- it "creates the namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).and_return(double(execute: true))
+ context "when current user can create namespaces" do
+ it "creates the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
- post :create, format: :js
+ expect { post :create, format: :js }.to change(Namespace, :count).by(1)
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, an_instance_of(Group), user, access_params).
+ and_return(double(execute: true))
- expect(Namespace.where(name: other_username).first).not_to be_nil
+ post :create, format: :js
+ end
end
- it "takes the new namespace" do
- expect(Gitlab::GithubImport::ProjectCreator).
- to receive(:new).with(github_repo, an_instance_of(Group), user, access_params).
- and_return(double(execute: true))
+ context "when current user can't create namespaces" do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
- post :create, format: :js
+ it "doesn't create the namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, format: :js }.not_to change(Namespace, :count)
+ end
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GithubImport::ProjectCreator).
+ to receive(:new).with(github_repo, user.namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
end
end
end
diff --git a/spec/controllers/import/gitlab_controller_spec.rb b/spec/controllers/import/gitlab_controller_spec.rb
index e8cf6aa7767..6f75ebb16c8 100644
--- a/spec/controllers/import/gitlab_controller_spec.rb
+++ b/spec/controllers/import/gitlab_controller_spec.rb
@@ -136,21 +136,42 @@ describe Import::GitlabController do
end
context "when a namespace with the GitLab.com user's username doesn't exist" do
- it "creates the namespace" do
- expect(Gitlab::GitlabImport::ProjectCreator).
- to receive(:new).and_return(double(execute: true))
+ context "when current user can create namespaces" do
+ it "creates the namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
- post :create, format: :js
+ expect { post :create, format: :js }.to change(Namespace, :count).by(1)
+ end
+
+ it "takes the new namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params).
+ and_return(double(execute: true))
- expect(Namespace.where(name: other_username).first).not_to be_nil
+ post :create, format: :js
+ end
end
- it "takes the new namespace" do
- expect(Gitlab::GitlabImport::ProjectCreator).
- to receive(:new).with(gitlab_repo, an_instance_of(Group), user, access_params).
- and_return(double(execute: true))
+ context "when current user can't create namespaces" do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
- post :create, format: :js
+ it "doesn't create the namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).and_return(double(execute: true))
+
+ expect { post :create, format: :js }.not_to change(Namespace, :count)
+ end
+
+ it "takes the current user's namespace" do
+ expect(Gitlab::GitlabImport::ProjectCreator).
+ to receive(:new).with(gitlab_repo, user.namespace, user, access_params).
+ and_return(double(execute: true))
+
+ post :create, format: :js
+ end
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 16929767ddf..90419368f22 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -370,6 +370,12 @@ describe Projects::IssuesController do
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
end
+
+ it 'delegates the update of the todos count cache to TodoService' do
+ expect_any_instance_of(TodoService).to receive(:destroy_issue).with(issue, owner).once
+
+ delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: issue.iid
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index a219400d75f..94c9edc91fe 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -320,6 +320,12 @@ describe Projects::MergeRequestsController do
expect(response).to have_http_status(302)
expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
end
+
+ it 'delegates the update of the todos count cache to TodoService' do
+ expect_any_instance_of(TodoService).to receive(:destroy_merge_request).with(merge_request, owner).once
+
+ delete :destroy, namespace_id: project.namespace.path, project_id: project.path, id: merge_request.iid
+ end
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index cccd492ef06..2e44b5128b4 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -49,4 +49,20 @@ describe Projects::ServicesController do
let!(:referrer) { nil }
end
end
+
+ describe 'PUT #update' do
+ context 'on successful update' do
+ it 'sets the flash' do
+ expect(service).to receive(:to_param).and_return('hipchat')
+
+ put :update,
+ namespace_id: project.namespace.id,
+ project_id: project.id,
+ id: service.id,
+ service: { active: false }
+
+ expect(flash[:notice]).to eq 'Successfully updated.'
+ end
+ end
+ end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 83e38095feb..6919002dedc 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -28,6 +28,11 @@ FactoryGirl.define do
diff_refs: noteable.diff_refs
)
end
+
+ trait :resolved do
+ resolved_at { Time.now }
+ resolved_by { create(:user) }
+ end
end
factory :diff_note_on_commit, traits: [:on_commit], class: DiffNote do
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 688f68d3cff..8863554ee91 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -211,6 +211,13 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding all diffs' do
before do
click_link('Expand all')
+
+ # Wait for elements to appear to ensure full page reload
+ expect(page).to have_content('This diff was suppressed by a .gitattributes entry')
+ expect(page).to have_content('This diff could not be displayed because it is too large.')
+ expect(page).to have_content('too_large_image.jpg')
+ find('.note-textarea')
+
wait_for_ajax
execute_script('window.ajaxUris = []; $(document).ajaxSend(function(event, xhr, settings) { ajaxUris.push(settings.url) });')
end
diff --git a/spec/features/issues/reset_filters_spec.rb b/spec/features/issues/reset_filters_spec.rb
new file mode 100644
index 00000000000..41f218eaa8b
--- /dev/null
+++ b/spec/features/issues/reset_filters_spec.rb
@@ -0,0 +1,81 @@
+require 'rails_helper'
+
+feature 'Issues filter reset button', feature: true, js: true do
+ include WaitForAjax
+ include IssueHelpers
+
+ let!(:project) { create(:project, :public) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:bug) { create(:label, project: project, name: 'bug')}
+ let!(:issue1) { create(:issue, project: project, milestone: milestone, author: user, assignee: user, title: 'Feature')}
+ let!(:issue2) { create(:labeled_issue, project: project, labels: [bug], title: 'Bugfix1')}
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ context 'when a milestone filter has been applied' do
+ it 'resets the milestone filter' do
+ visit_issues(project, milestone_title: milestone.title)
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when a label filter has been applied' do
+ it 'resets the label filter' do
+ visit_issues(project, label_name: bug.name)
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when a text search has been conducted' do
+ it 'resets the text search filter' do
+ visit_issues(project, issue_search: 'Bug')
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when author filter has been applied' do
+ it 'resets the author filter' do
+ visit_issues(project, author_id: user.id)
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when assignee filter has been applied' do
+ it 'resets the assignee filter' do
+ visit_issues(project, assignee_id: user.id)
+ expect(page).to have_css('.issue', count: 1)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ context 'when all filters have been applied' do
+ it 'resets all filters' do
+ visit_issues(project, assignee_id: user.id, author_id: user.id, milestone_title: milestone.title, label_name: bug.name, issue_search: 'Bug')
+ expect(page).to have_css('.issue', count: 0)
+
+ reset_filters
+ expect(page).to have_css('.issue', count: 2)
+ end
+ end
+
+ def reset_filters
+ find('.reset-filters').click
+ end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 2883e392694..105629c485a 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
feature 'Issues > User uses slash commands', feature: true, js: true do
+ include SlashCommandsHelpers
include WaitForAjax
it_behaves_like 'issuable record that supports slash commands in its description and notes', :issue do
@@ -17,14 +18,15 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
visit namespace_project_issue_path(project.namespace, project, issue)
end
+ after do
+ wait_for_ajax
+ end
+
describe 'adding a due date from note' do
let(:issue) { create(:issue, project: project) }
it 'does not create a note, and sets the due date accordingly' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/due 2016-08-28"
- click_button 'Comment'
- end
+ write_note("/due 2016-08-28")
expect(page).not_to have_content '/due 2016-08-28'
expect(page).to have_content 'Your commands have been executed!'
@@ -41,10 +43,7 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
it 'does not create a note, and removes the due date accordingly' do
expect(issue.due_date).to eq Date.new(2016, 8, 28)
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/remove_due_date"
- click_button 'Comment'
- end
+ write_note("/remove_due_date")
expect(page).not_to have_content '/remove_due_date'
expect(page).to have_content 'Your commands have been executed!'
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
index 577c910f11b..9e759de3752 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -11,8 +11,8 @@ feature 'Merge Request versions', js: true, feature: true do
end
it 'show the latest version of the diff' do
- page.within '.mr-version-switch' do
- expect(page).to have_content 'Version: latest'
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'latest version'
end
expect(page).to have_content '8 changed files'
@@ -20,18 +20,49 @@ feature 'Merge Request versions', js: true, feature: true do
describe 'switch between versions' do
before do
- page.within '.mr-version-switch' do
+ page.within '.mr-version-dropdown' do
find('.btn-link').click
- click_link '6f6d7e7e'
+ click_link 'version 1'
end
end
it 'should show older version' do
- page.within '.mr-version-switch' do
- expect(page).to have_content 'Version: 6f6d7e7e'
+ page.within '.mr-version-dropdown' do
+ expect(page).to have_content 'version 1'
end
expect(page).to have_content '5 changed files'
end
+
+ it 'show the message about disabled comments' do
+ expect(page).to have_content 'Comments are disabled'
+ end
+ end
+
+ describe 'compare with older version' do
+ before do
+ page.within '.mr-version-compare-dropdown' do
+ find('.btn-link').click
+ click_link 'version 1'
+ end
+ end
+
+ it 'should has correct value in the compare dropdown' do
+ page.within '.mr-version-compare-dropdown' do
+ expect(page).to have_content 'version 1'
+ end
+ end
+
+ it 'show the message about disabled comments' do
+ expect(page).to have_content 'Comments are disabled'
+ end
+
+ it 'show diff between new and old version' do
+ expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
+ end
+
+ it 'show diff between new and old version' do
+ expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
+ end
end
end
diff --git a/spec/features/merge_requests/update_merge_requests_spec.rb b/spec/features/merge_requests/update_merge_requests_spec.rb
new file mode 100644
index 00000000000..b56fdfe5611
--- /dev/null
+++ b/spec/features/merge_requests/update_merge_requests_spec.rb
@@ -0,0 +1,132 @@
+require 'rails_helper'
+
+feature 'Multiple merge requests updating from merge_requests#index', feature: true do
+ include WaitForAjax
+
+ let!(:user) { create(:user)}
+ let!(:project) { create(:project) }
+ let!(:merge_request) { create(:merge_request, source_project: project, target_project: project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'status', js: true do
+ describe 'close merge request' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it 'closes merge request' do
+ change_status('Closed')
+
+ expect(page).to have_selector('.merge-request', count: 0)
+ end
+ end
+
+ describe 'reopen merge request' do
+ before do
+ merge_request.close
+ visit namespace_project_merge_requests_path(project.namespace, project, state: 'closed')
+ end
+
+ it 'reopens merge request' do
+ change_status('Open')
+
+ expect(page).to have_selector('.merge-request', count: 0)
+ end
+ end
+ end
+
+ context 'assignee', js: true do
+ describe 'set assignee' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it "updates merge request with assignee" do
+ change_assignee(user.name)
+
+ page.within('.merge-request .controls') do
+ expect(find('.author_link')["title"]).to have_content(user.name)
+ end
+ end
+ end
+
+ describe 'remove assignee' do
+ before do
+ merge_request.assignee = user
+ merge_request.save
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it "removes assignee from the merge request" do
+ change_assignee('Unassigned')
+
+ expect(find('.merge-request .controls')).not_to have_css('.author_link')
+ end
+ end
+ end
+
+ context 'milestone', js: true do
+ let(:milestone) { create(:milestone, project: project) }
+
+ describe 'set milestone' do
+ before do
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it "updates merge request with milestone" do
+ change_milestone(milestone.title)
+
+ expect(find('.merge-request')).to have_content milestone.title
+ end
+ end
+
+ describe 'unset milestone' do
+ before do
+ merge_request.milestone = milestone
+ merge_request.save
+ visit namespace_project_merge_requests_path(project.namespace, project)
+ end
+
+ it "removes milestone from the merge request" do
+ change_milestone("No Milestone")
+
+ expect(find('.merge-request')).not_to have_content milestone.title
+ end
+ end
+ end
+
+ def change_status(text)
+ find('#check_all_issues').click
+ find('.js-issue-status').click
+ find('.dropdown-menu-status a', text: text).click
+ click_update_merge_requests_button
+ end
+
+ def change_assignee(text)
+ find('#check_all_issues').click
+ find('.js-update-assignee').click
+ wait_for_ajax
+
+ page.within '.dropdown-menu-user' do
+ click_link text
+ end
+
+ click_update_merge_requests_button
+ end
+
+ def change_milestone(text)
+ find('#check_all_issues').click
+ find('.issues_bulk_update .js-milestone-select').click
+ find('.dropdown-menu-milestone a', text: text).click
+ click_update_merge_requests_button
+ end
+
+ def click_update_merge_requests_button
+ find('.update_selected_issues').click
+ wait_for_ajax
+ end
+end
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index d9ef0d18074..22d9d1b9fd5 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -1,6 +1,7 @@
require 'rails_helper'
feature 'Merge Requests > User uses slash commands', feature: true, js: true do
+ include SlashCommandsHelpers
include WaitForAjax
let(:user) { create(:user) }
@@ -20,11 +21,12 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
+ after do
+ wait_for_ajax
+ end
+
it 'does not recognize the command nor create a note' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/due 2016-08-28"
- click_button 'Comment'
- end
+ write_note("/due 2016-08-28")
expect(page).not_to have_content '/due 2016-08-28'
end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index 0cfeb2e57d8..d1685f95503 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+require 'tempfile'
describe "Builds" do
let(:artifacts_file) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
@@ -6,7 +7,7 @@ describe "Builds" do
before do
login_as(:user)
@commit = FactoryGirl.create :ci_pipeline
- @build = FactoryGirl.create :ci_build, pipeline: @commit
+ @build = FactoryGirl.create :ci_build, :trace, pipeline: @commit
@build2 = FactoryGirl.create :ci_build
@project = @commit.project
@project.team << [@user, :developer]
@@ -156,7 +157,6 @@ describe "Builds" do
context 'Build raw trace' do
before do
@build.run!
- @build.trace = 'BUILD TRACE'
visit namespace_project_build_path(@project.namespace, @project, @build)
end
@@ -164,6 +164,26 @@ describe "Builds" do
expect(page).to have_link 'Raw'
end
end
+
+ describe 'Variables' do
+ before do
+ @trigger_request = create :ci_trigger_request_with_variables
+ @build = create :ci_build, pipeline: @commit, trigger_request: @trigger_request
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it 'shows variable key and value after click', js: true do
+ expect(page).to have_css('.reveal-variables')
+ expect(page).not_to have_css('.js-build-variable')
+ expect(page).not_to have_css('.js-build-value')
+
+ click_button 'Reveal Variables'
+
+ expect(page).not_to have_css('.reveal-variables')
+ expect(page).to have_selector('.js-build-variable', text: 'TRIGGER_KEY_1')
+ expect(page).to have_selector('.js-build-value', text: 'TRIGGER_VALUE_1')
+ end
+ end
end
describe "POST /:project/builds/:id/cancel" do
@@ -255,35 +275,101 @@ describe "Builds" do
end
end
- describe "GET /:project/builds/:id/raw" do
- context "Build from project" do
- before do
- Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build.run!
- @build.trace = 'BUILD TRACE'
- visit namespace_project_build_path(@project.namespace, @project, @build)
- page.within('.js-build-sidebar') { click_link 'Raw' }
+ describe 'GET /:project/builds/:id/raw' do
+ context 'access source' do
+ context 'build from project' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ end
end
- it 'sends the right headers' do
- expect(page.status_code).to eq(200)
- expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ context 'build from other project' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build2.run!
+ visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(404)
+ end
end
end
- context "Build from other project" do
- before do
- Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
- @build2.run!
- @build2.trace = 'BUILD TRACE'
- visit raw_namespace_project_build_path(@project.namespace, @project, @build2)
- puts page.status_code
- puts current_url
+ context 'storage form' do
+ let(:existing_file) { Tempfile.new('existing-trace-file').path }
+ let(:non_existing_file) do
+ file = Tempfile.new('non-existing-trace-file')
+ path = file.path
+ file.unlink
+ path
end
- it 'sends the right headers' do
- expect(page.status_code).to eq(404)
+ context 'when build has trace in file' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ end
+ end
+
+ context 'when build has trace in old file' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(999)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(200)
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(existing_file)
+ end
+ end
+
+ context 'when build has trace in DB' do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+
+ allow_any_instance_of(Project).to receive(:ci_id).and_return(nil)
+ allow_any_instance_of(Ci::Build).to receive(:path_to_trace).and_return(non_existing_file)
+ allow_any_instance_of(Ci::Build).to receive(:old_path_to_trace).and_return(non_existing_file)
+
+ page.within('.js-build-sidebar') { click_link 'Raw' }
+ end
+
+ it 'sends the right headers' do
+ expect(page.status_code).to eq(404)
+ end
end
end
end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
new file mode 100644
index 00000000000..a1643fd1f43
--- /dev/null
+++ b/spec/features/projects/edit_spec.rb
@@ -0,0 +1,57 @@
+require 'rails_helper'
+
+feature 'Project edit', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+
+ visit edit_namespace_project_path(project.namespace, project)
+ end
+
+ context 'feature visibility' do
+ context 'merge requests select' do
+ it 'hides merge requests section' do
+ select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+ end
+
+ it 'hides merge requests section after save' do
+ select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+
+ click_button 'Save changes'
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.merge-requests-feature', visible: false)
+ end
+ end
+
+ context 'builds select' do
+ it 'hides merge requests section' do
+ select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+ end
+
+ it 'hides merge requests section after save' do
+ select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+
+ click_button 'Save changes'
+
+ wait_for_ajax
+
+ expect(page).to have_selector('.builds-feature', visible: false)
+ end
+ end
+ end
+end
diff --git a/spec/features/triggers_spec.rb b/spec/features/triggers_spec.rb
index 3cbc8253ad6..72354834c5a 100644
--- a/spec/features/triggers_spec.rb
+++ b/spec/features/triggers_spec.rb
@@ -12,7 +12,7 @@ describe 'Triggers' do
context 'create a trigger' do
before do
- click_on 'Add Trigger'
+ click_on 'Add trigger'
expect(@project.triggers.count).to eq(1)
end
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
new file mode 100644
index 00000000000..356a8d668b0
--- /dev/null
+++ b/spec/features/users/snippets_spec.rb
@@ -0,0 +1,29 @@
+require 'spec_helper'
+
+describe 'Snippets tab on a user profile', feature: true, js: true do
+ include WaitForAjax
+
+ let(:user) { create(:user) }
+
+ context 'when the user has snippets' do
+ before do
+ create_list(:snippet, 25, :public, author: user)
+
+ visit user_path(user)
+ page.within('.user-profile-nav') { click_link 'Snippets' }
+ wait_for_ajax
+ end
+
+ it 'is limited to 20 items per page' do
+ expect(page.all('.snippets-list-holder .snippet-row').count).to eq(20)
+ end
+
+ context 'clicking on the link to the second page' do
+ before { click_link('2') }
+
+ it 'shows the remaining snippets' do
+ expect(page.all('.snippets-list-holder .snippet-row').count).to eq(5)
+ end
+ end
+ end
+end
diff --git a/spec/finders/pipelines_finder_spec.rb b/spec/finders/pipelines_finder_spec.rb
new file mode 100644
index 00000000000..7100266ab55
--- /dev/null
+++ b/spec/finders/pipelines_finder_spec.rb
@@ -0,0 +1,51 @@
+require 'spec_helper'
+
+describe PipelinesFinder do
+ let(:project) { create(:project) }
+
+ let!(:tag_pipeline) { create(:ci_pipeline, project: project, ref: 'v1.0.0') }
+ let!(:branch_pipeline) { create(:ci_pipeline, project: project) }
+
+ subject { described_class.new(project).execute(params) }
+
+ describe "#execute" do
+ context 'when a scope is passed' do
+ context 'when scope is nil' do
+ let(:params) { { scope: nil } }
+
+ it 'selects all pipelines' do
+ expect(subject.count).to be 2
+ expect(subject).to include tag_pipeline
+ expect(subject).to include branch_pipeline
+ end
+ end
+
+ context 'when selecting branches' do
+ let(:params) { { scope: 'branches' } }
+
+ it 'excludes tags' do
+ expect(subject).not_to include tag_pipeline
+ expect(subject).to include branch_pipeline
+ end
+ end
+
+ context 'when selecting tags' do
+ let(:params) { { scope: 'tags' } }
+
+ it 'excludes branches' do
+ expect(subject).to include tag_pipeline
+ expect(subject).not_to include branch_pipeline
+ end
+ end
+ end
+
+ # Scoping to running will speed up the test as it doesn't hit the FS
+ let(:params) { { scope: 'running' } }
+
+ it 'orders in descending order on ID' do
+ create(:ci_pipeline, project: project, ref: 'feature')
+
+ expect(subject.map(&:id)).to eq [3, 2, 1]
+ end
+ end
+end
diff --git a/spec/helpers/git_helper_spec.rb b/spec/helpers/git_helper_spec.rb
new file mode 100644
index 00000000000..9b1ef1e05a2
--- /dev/null
+++ b/spec/helpers/git_helper_spec.rb
@@ -0,0 +1,9 @@
+require 'spec_helper'
+
+describe GitHelper do
+ describe '#short_sha' do
+ let(:short_sha) { helper.short_sha('d4e043f6c20749a3ab3f4b8e23f2a8979f4b9100') }
+
+ it { expect(short_sha).to eq('d4e043f6') }
+ end
+end
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
index 3391234e9f5..187b891b927 100644
--- a/spec/helpers/import_helper_spec.rb
+++ b/spec/helpers/import_helper_spec.rb
@@ -1,6 +1,30 @@
require 'rails_helper'
describe ImportHelper do
+ describe '#import_project_target' do
+ let(:user) { create(:user) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ end
+
+ context 'when current user can create namespaces' do
+ it 'returns project namespace' do
+ user.update_attribute(:can_create_group, true)
+
+ expect(helper.import_project_target('asd', 'vim')).to eq 'asd/vim'
+ end
+ end
+
+ context 'when current user can not create namespaces' do
+ it "takes the current user's namespace" do
+ user.update_attribute(:can_create_group, false)
+
+ expect(helper.import_project_target('asd', 'vim')).to eq "#{user.namespace_path}/vim"
+ end
+ end
+ end
+
describe '#github_project_link' do
context 'when provider does not specify a custom URL' do
it 'uses default GitHub URL' do
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
deleted file mode 100644
index e4d18d8bfc6..00000000000
--- a/spec/helpers/nav_helper_spec.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-require 'spec_helper'
-
-# Specs in this file have access to a helper object that includes
-# the NavHelper. For example:
-#
-# describe NavHelper do
-# describe "string concat" do
-# it "concats two strings with spaces" do
-# expect(helper.concat_strings("this","that")).to eq("this that")
-# end
-# end
-# end
-describe NavHelper do
- describe '#nav_menu_collapsed?' do
- it 'returns true when the nav is collapsed in the cookie' do
- helper.request.cookies[:collapsed_nav] = 'true'
- expect(helper.nav_menu_collapsed?).to eq true
- end
-
- it 'returns false when the nav is not collapsed in the cookie' do
- helper.request.cookies[:collapsed_nav] = 'false'
- expect(helper.nav_menu_collapsed?).to eq false
- end
- end
-end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 284b58d8d5c..70032e7df94 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -174,4 +174,48 @@ describe ProjectsHelper do
end
end
end
+
+ describe "#project_feature_access_select" do
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ context "when project is internal or public" do
+ it "shows all options" do
+ helper.instance_variable_set(:@project, project)
+ result = helper.project_feature_access_select(:issues_access_level)
+ expect(result).to include("Disabled")
+ expect(result).to include("Only team members")
+ expect(result).to include("Everyone with access")
+ end
+ end
+
+ context "when project is private" do
+ before { project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+
+ it "shows only allowed options" do
+ helper.instance_variable_set(:@project, project)
+ result = helper.project_feature_access_select(:issues_access_level)
+ expect(result).to include("Disabled")
+ expect(result).to include("Only team members")
+ expect(result).not_to include("Everyone with access")
+ end
+ end
+
+ context "when project moves from public to private" do
+ before do
+ project.project_feature.update_attributes(issues_access_level: ProjectFeature::ENABLED)
+ project.update_attributes(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ end
+
+ it "shows the highest allowed level selected" do
+ helper.instance_variable_set(:@project, project)
+ result = helper.project_feature_access_select(:issues_access_level)
+
+ expect(result).to include("Disabled")
+ expect(result).to include("Only team members")
+ expect(result).not_to include("Everyone with access")
+ expect(result).to have_selector('option[selected]', text: "Only team members")
+ end
+ end
+ end
end
diff --git a/spec/helpers/search_helper_spec.rb b/spec/helpers/search_helper_spec.rb
index b0bb991539b..4b2ca3514f8 100644
--- a/spec/helpers/search_helper_spec.rb
+++ b/spec/helpers/search_helper_spec.rb
@@ -32,6 +32,10 @@ describe SearchHelper do
expect(search_autocomplete_opts("adm").size).to eq(1)
end
+ it "does not allow regular expression in search term" do
+ expect(search_autocomplete_opts("(webhooks|api)").size).to eq(0)
+ end
+
it "includes the user's groups" do
create(:group).add_owner(user)
expect(search_autocomplete_opts("gro").size).to eq(1)
diff --git a/spec/helpers/sidekiq_helper_spec.rb b/spec/helpers/sidekiq_helper_spec.rb
new file mode 100644
index 00000000000..d60839b78ec
--- /dev/null
+++ b/spec/helpers/sidekiq_helper_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe SidekiqHelper do
+ describe 'parse_sidekiq_ps' do
+ it 'parses line with time' do
+ line = '55137 10,0 2,1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['55137', '10,0', '2,1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+ end
+
+ it 'parses line with date' do
+ line = '55137 10,0 2,1 S+ Aug 4 sidekiq 4.1.4 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 4', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+ end
+
+ it 'parses line with two digit date' do
+ line = '55137 10,0 2,1 S+ Aug 04 sidekiq 4.1.4 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['55137', '10,0', '2,1', 'S+', 'Aug 04', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+ end
+
+ it 'parses line with dot as float separator' do
+ line = '55137 10.0 2.1 S+ 2:30pm sidekiq 4.1.4 gitlab [0 of 25 busy] '
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['55137', '10.0', '2.1', 'S+', '2:30pm', 'sidekiq 4.1.4 gitlab [0 of 25 busy]'])
+ end
+
+ it 'does fail gracefully on line not matching the format' do
+ line = '55137 10.0 2.1 S+ 2:30pm something'
+ parts = helper.parse_sidekiq_ps(line)
+
+ expect(parts).to eq(['?', '?', '?', '?', '?', '?'])
+ end
+ end
+end
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index c1c12b57b53..019ce3b0702 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,13 +1,7 @@
/*= require awards_handler */
-
-
/*= require jquery */
-
-
/*= require jquery.cookie */
-
-
/*= require ./fixtures/emoji_menu */
(function() {
@@ -33,6 +27,7 @@
return setTimeout(function() {
assertFn();
return done();
+ // Maybe jasmine.clock here?
}, 333);
};
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 4c52ecd903d..13babb5bfdb 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -8,6 +8,7 @@
beforeEach(function() {
fixture.load('behaviors/quick_submit.html');
$('form').submit(function(e) {
+ // Prevent a form submit from moving us off the testing page
return e.preventDefault();
});
return this.spies = {
@@ -38,6 +39,8 @@
expect($('input[type=submit]')).toBeDisabled();
return expect($('button[type=submit]')).toBeDisabled();
});
+ // We cannot stub `navigator.userAgent` for CI's `rake teaspoon` task, so we'll
+ // only run the tests that apply to the current platform
if (navigator.userAgent.match(/Macintosh/)) {
it('responds to Meta+Enter', function() {
$('input.quick-submit-input').trigger(keydownEvent());
diff --git a/spec/javascripts/datetime_utility_spec.js.coffee b/spec/javascripts/datetime_utility_spec.js.coffee
deleted file mode 100644
index 8bd113e7d86..00000000000
--- a/spec/javascripts/datetime_utility_spec.js.coffee
+++ /dev/null
@@ -1,50 +0,0 @@
-#= require lib/utils/datetime_utility
-
-describe 'Date time utils', ->
- describe 'get day name', ->
- it 'should return Sunday', ->
- day = gl.utils.getDayName(new Date('07/17/2016'))
- expect(day).toBe('Sunday')
-
- it 'should return Monday', ->
- day = gl.utils.getDayName(new Date('07/18/2016'))
- expect(day).toBe('Monday')
-
- it 'should return Tuesday', ->
- day = gl.utils.getDayName(new Date('07/19/2016'))
- expect(day).toBe('Tuesday')
-
- it 'should return Wednesday', ->
- day = gl.utils.getDayName(new Date('07/20/2016'))
- expect(day).toBe('Wednesday')
-
- it 'should return Thursday', ->
- day = gl.utils.getDayName(new Date('07/21/2016'))
- expect(day).toBe('Thursday')
-
- it 'should return Friday', ->
- day = gl.utils.getDayName(new Date('07/22/2016'))
- expect(day).toBe('Friday')
-
- 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/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
new file mode 100644
index 00000000000..a2d1b0a7732
--- /dev/null
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -0,0 +1,64 @@
+//= require lib/utils/datetime_utility
+(() => {
+ describe('Date time utils', () => {
+ describe('get day name', () => {
+ it('should return Sunday', () => {
+ const day = gl.utils.getDayName(new Date('07/17/2016'));
+ expect(day).toBe('Sunday');
+ });
+
+ it('should return Monday', () => {
+ const day = gl.utils.getDayName(new Date('07/18/2016'));
+ expect(day).toBe('Monday');
+ });
+
+ it('should return Tuesday', () => {
+ const day = gl.utils.getDayName(new Date('07/19/2016'));
+ expect(day).toBe('Tuesday');
+ });
+
+ it('should return Wednesday', () => {
+ const day = gl.utils.getDayName(new Date('07/20/2016'));
+ expect(day).toBe('Wednesday');
+ });
+
+ it('should return Thursday', () => {
+ const day = gl.utils.getDayName(new Date('07/21/2016'));
+ expect(day).toBe('Thursday');
+ });
+
+ it('should return Friday', () => {
+ const day = gl.utils.getDayName(new Date('07/22/2016'));
+ expect(day).toBe('Friday');
+ });
+
+ it('should return Saturday', () => {
+ const day = gl.utils.getDayName(new Date('07/23/2016'));
+ expect(day).toBe('Saturday');
+ });
+ });
+
+ describe('get day difference', () => {
+ it('should return 7', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('07/08/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(7);
+ });
+
+ it('should return 31', () => {
+ const firstDay = new Date('07/01/2016');
+ const secondDay = new Date('08/01/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(31);
+ });
+
+ it('should return 365', () => {
+ const firstDay = new Date('07/02/2015');
+ const secondDay = new Date('07/01/2016');
+ const difference = gl.utils.getDayDifference(firstDay, secondDay);
+ expect(difference).toBe(365);
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/fixtures/comments.html.haml b/spec/javascripts/fixtures/comments.html.haml
new file mode 100644
index 00000000000..cc1f8f15c21
--- /dev/null
+++ b/spec/javascripts/fixtures/comments.html.haml
@@ -0,0 +1,21 @@
+.flash-container.timeline-content
+.timeline-icon.hidden-xs.hidden-sm
+ %a.author_link
+ %img
+.timeline-content.timeline-content-form
+ %form.new-note.js-quick-submit.common-note-form.gfm-form.js-main-target-form
+ .md-area
+ .md-header
+ .md-write-holder
+ .zen-backdrop.div-dropzone-wrapper
+ .div-dropzone-wrapper
+ .div-dropzone.dz-clickable
+ %textarea.note-textarea.js-note-text.js-gfm-input.js-autosize.markdown-area
+ .note-form-actions.clearfix
+ %input.btn.btn-nr.btn-create.append-right-10.comment-btn.js-comment-button{ type: 'submit' }
+ %a.btn.btn-nr.btn-reopen.btn-comment.js-note-target-reopen
+ Reopen issue
+ %a.btn.btn-nr.btn-close.btn-comment.js-note-target-close
+ Close issue
+ %a.btn.btn-cancel.js-note-discard
+ Discard draft \ No newline at end of file
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 82ee1954a59..d5401fbb0d1 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -7,7 +7,7 @@ describe("ContributorsGraph", function () {
expect(ContributorsGraph.prototype.x_domain).toEqual(20)
})
})
-
+
describe("#set_y_domain", function () {
it("sets the y_domain", function () {
ContributorsGraph.set_y_domain([{commits: 30}])
@@ -89,7 +89,7 @@ describe("ContributorsGraph", function () {
})
describe("ContributorsMasterGraph", function () {
-
+
// TODO: fix or remove
//describe("#process_dates", function () {
//it("gets and parses dates", function () {
@@ -103,7 +103,7 @@ describe("ContributorsMasterGraph", function () {
//expect(graph.get_dates).toHaveBeenCalledWith(data)
//expect(ContributorsGraph.set_dates).toHaveBeenCalledWith("get")
//})
- //})
+ //})
describe("#get_dates", function () {
it("plucks the date field from data collection", function () {
@@ -124,5 +124,5 @@ describe("ContributorsMasterGraph", function () {
})
})
-
+
})
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index dc6231ebb38..33690c7a5f3 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,7 +1,5 @@
/*= require lib/utils/text_utility */
-
-
/*= require issue */
(function() {
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index 25d3f5b6c04..f09596bd36d 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,7 +1,5 @@
/*= require jquery-ui/autocomplete */
-
-
/*= require new_branch_form */
(function() {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 14dc6bfdfde..a588f403dd5 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,8 +1,7 @@
-
/*= require notes */
-
-
+/*= require autosize */
/*= require gl_form */
+/*= require lib/utils/text_utility */
(function() {
window.gon || (window.gon = {});
@@ -12,29 +11,63 @@
};
describe('Notes', function() {
- return describe('task lists', function() {
+ describe('task lists', function() {
fixture.preload('issue_note.html');
+
beforeEach(function() {
fixture.load('issue_note.html');
$('form').on('submit', function(e) {
- return e.preventDefault();
+ e.preventDefault();
});
- return this.notes = new Notes();
+ this.notes = new Notes();
});
+
it('modifies the Markdown field', function() {
$('input[type=checkbox]').attr('checked', true).trigger('change');
- return expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
+ expect($('.js-task-list-field').val()).toBe('- [x] Task List Item');
});
- return it('submits the form on tasklist:changed', function() {
- var submitted;
- submitted = false;
+
+ it('submits the form on tasklist:changed', function() {
+ var submitted = false;
$('form').on('submit', function(e) {
submitted = true;
- return e.preventDefault();
+ e.preventDefault();
});
+
$('.js-task-list-field').trigger('tasklist:changed');
- return expect(submitted).toBe(true);
+ expect(submitted).toBe(true);
+ });
+ });
+
+ describe('comments', function() {
+ var commentsTemplate = 'comments.html';
+ var textarea = '.js-note-text';
+ fixture.preload(commentsTemplate);
+
+ beforeEach(function() {
+ fixture.load(commentsTemplate);
+ this.notes = new Notes();
+
+ this.autoSizeSpy = spyOnEvent($(textarea), 'autosize:update');
+ spyOn(this.notes, 'renderNote').and.stub();
+
+ $(textarea).data('autosave', {
+ reset: function() {}
+ });
+
+ $('form').on('submit', function(e) {
+ e.preventDefault();
+ $('.js-main-target-form').trigger('ajax:success');
+ });
});
+
+ it('autosizes after comment submission', function() {
+ $(textarea).text('This is an example comment note');
+ expect(this.autoSizeSpy).not.toHaveBeenTriggered();
+
+ $('.js-comment-button').click();
+ expect(this.autoSizeSpy).toHaveBeenTriggered();
+ })
});
});
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index ffe49828492..51eb12b41d4 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,22 +1,10 @@
/*= require bootstrap */
-
-
/*= require select2 */
-
-
/*= require lib/utils/type_utility */
-
-
/*= require gl_dropdown */
-
-
/*= require api */
-
-
/*= require project_select */
-
-
/*= require project */
(function() {
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index 38b3b2653ec..c937a4706f7 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,10 +1,6 @@
/*= require right_sidebar */
-
-
/*= require jquery */
-
-
/*= require jquery.cookie */
(function() {
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 324f5152780..00d9fc1302a 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,19 +1,9 @@
/*= require gl_dropdown */
-
-
/*= require search_autocomplete */
-
-
/*= require jquery */
-
-
/*= require lib/utils/common_utils */
-
-
/*= require lib/utils/type_utility */
-
-
/*= require fuzzaldrin-plus */
(function() {
@@ -43,6 +33,8 @@
groupName = 'Gitlab Org';
+ // Add required attributes to body before starting the test.
+ // section would be dashboard|group|project
addBodyAttributes = function(section) {
var $body;
if (section == null) {
@@ -64,6 +56,7 @@
}
};
+ // Mock `gl` object in window for dashboard specific page. App code will need it.
mockDashboardOptions = function() {
window.gl || (window.gl = {});
return window.gl.dashboardOptions = {
@@ -72,6 +65,7 @@
};
};
+ // Mock `gl` object in window for project specific page. App code will need it.
mockProjectOptions = function() {
window.gl || (window.gl = {});
return window.gl.projectOptions = {
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 7b6b55fe545..04ccf246052 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -10,6 +10,7 @@
});
return describe('#replyWithSelectedText', function() {
var stubSelection;
+ // Stub window.getSelection to return the provided String.
stubSelection = function(text) {
return window.getSelection = function() {
return text;
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
index 7d91ed0f855..8801c297887 100644
--- a/spec/javascripts/spec_helper.js
+++ b/spec/javascripts/spec_helper.js
@@ -1,21 +1,41 @@
-
+// PhantomJS (Teaspoons default driver) doesn't have support for
+// Function.prototype.bind, which has caused confusion. Use this polyfill to
+// avoid the confusion.
/*= require support/bind-poly */
-
+// You can require your own javascript files here. By default this will include
+// everything in application, however you may get better load performance if you
+// require the specific files that are being used in the spec that tests them.
/*= require jquery */
-
-
/*= require jquery.turbolinks */
-
-
/*= require bootstrap */
-
-
/*= require underscore */
-
+// Teaspoon includes some support files, but you can use anything from your own
+// support path too.
+// require support/jasmine-jquery-1.7.0
+// require support/jasmine-jquery-2.0.0
/*= require support/jasmine-jquery-2.1.0 */
+// require support/sinon
+// require support/your-support-file
+// Deferring execution
+// If you're using CommonJS, RequireJS or some other asynchronous library you can
+// defer execution. Call Teaspoon.execute() after everything has been loaded.
+// Simple example of a timeout:
+// Teaspoon.defer = true
+// setTimeout(Teaspoon.execute, 1000)
+// Matching files
+// By default Teaspoon will look for files that match
+// _spec.{js,js.coffee,.coffee}. Add a filename_spec.js file in your spec path
+// and it'll be included in the default suite automatically. If you want to
+// customize suites, check out the configuration in teaspoon_env.rb
+// Manifest
+// If you'd rather require your spec files manually (to control order for
+// instance) you can disable the suite matcher in the configuration and use this
+// file as a manifest.
+// For more information: http://github.com/modeset/teaspoon
+
(function() {
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index e008ce956ad..7ce3884f844 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,16 +1,8 @@
/*= require u2f/authenticate */
-
-
/*= require u2f/util */
-
-
/*= require u2f/error */
-
-
/*= require u2f */
-
-
/*= require ./mock_u2f_device */
(function() {
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 21c5266c60e..01d6b7a8961 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,16 +1,8 @@
/*= require u2f/register */
-
-
/*= require u2f/util */
-
-
/*= require u2f/error */
-
-
/*= require u2f */
-
-
/*= require ./mock_u2f_device */
(function() {
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 3d680ec8ea3..0c1266800d7 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -14,8 +14,10 @@
return true;
}
};
+ // Stub Dropzone.forElement(...).enable()
});
this.zen = new ZenMode();
+ // Set this manually because we can't actually scroll the window
return this.zen.scroll_position = 456;
});
describe('on enter', function() {
@@ -60,7 +62,7 @@
return $('a.js-zen-enter').click();
};
- exitZen = function() {
+ exitZen = function() { // Ohmmmmmmm
return $('a.js-zen-leave').click();
};
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index be51d942af7..af192664b33 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -1250,5 +1250,40 @@ EOT
end
end
end
+
+ describe "#validation_message" do
+ context "when the YAML could not be parsed" do
+ it "returns an error about invalid configutaion" do
+ content = YAML.dump("invalid: yaml: test")
+
+ expect(GitlabCiYamlProcessor.validation_message(content))
+ .to eq "Invalid configuration format"
+ end
+ end
+
+ context "when the tags parameter is invalid" do
+ it "returns an error about invalid tags" do
+ content = YAML.dump({ rspec: { script: "test", tags: "mysql" } })
+
+ expect(GitlabCiYamlProcessor.validation_message(content))
+ .to eq "jobs:rspec tags should be an array of strings"
+ end
+ end
+
+ context "when YAML content is empty" do
+ it "returns an error about missing content" do
+ expect(GitlabCiYamlProcessor.validation_message(''))
+ .to eq "Please provide content of .gitlab-ci.yml"
+ end
+ end
+
+ context "when the YAML is valid" do
+ it "does not return any errors" do
+ content = File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+
+ expect(GitlabCiYamlProcessor.validation_message(content)).to be_nil
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/cache_spec.rb b/spec/lib/gitlab/ci/config/node/cache_spec.rb
index 50f619ce26e..e251210949c 100644
--- a/spec/lib/gitlab/ci/config/node/cache_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/cache_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Cache do
let(:entry) { described_class.new(config) }
describe 'validations' do
- before { entry.process! }
+ before { entry.compose! }
context 'when entry config value is correct' do
let(:config) do
diff --git a/spec/lib/gitlab/ci/config/node/factory_spec.rb b/spec/lib/gitlab/ci/config/node/factory_spec.rb
index d26185ba585..a699089c563 100644
--- a/spec/lib/gitlab/ci/config/node/factory_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/factory_spec.rb
@@ -65,7 +65,8 @@ describe Gitlab::Ci::Config::Node::Factory do
.value(nil)
.create!
- expect(entry).to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
+ expect(entry)
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
end
end
diff --git a/spec/lib/gitlab/ci/config/node/global_spec.rb b/spec/lib/gitlab/ci/config/node/global_spec.rb
index 2f87d270b36..12232ff7e2f 100644
--- a/spec/lib/gitlab/ci/config/node/global_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/global_spec.rb
@@ -14,7 +14,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when hash is valid' do
- context 'when all entries defined' do
+ context 'when some entries defined' do
let(:hash) do
{ before_script: ['ls', 'pwd'],
image: 'ruby:2.2',
@@ -24,11 +24,11 @@ describe Gitlab::Ci::Config::Node::Global do
stages: ['build', 'pages'],
cache: { key: 'k', untracked: true, paths: ['public/'] },
rspec: { script: %w[rspec ls] },
- spinach: { script: 'spinach' } }
+ spinach: { before_script: [], variables: {}, script: 'spinach' } }
end
- describe '#process!' do
- before { global.process! }
+ describe '#compose!' do
+ before { global.compose! }
it 'creates nodes hash' do
expect(global.descendants).to be_an Array
@@ -59,7 +59,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
- context 'when not processed' do
+ context 'when not composed' do
describe '#before_script' do
it 'returns nil' do
expect(global.before_script).to be nil
@@ -73,8 +73,14 @@ describe Gitlab::Ci::Config::Node::Global do
end
end
- context 'when processed' do
- before { global.process! }
+ context 'when composed' do
+ before { global.compose! }
+
+ describe '#errors' do
+ it 'has no errors' do
+ expect(global.errors).to be_empty
+ end
+ end
describe '#before_script' do
it 'returns correct script' do
@@ -137,10 +143,24 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.jobs).to eq(
rspec: { name: :rspec,
script: %w[rspec ls],
- stage: 'test' },
+ before_script: ['ls', 'pwd'],
+ commands: "ls\npwd\nrspec\nls",
+ image: 'ruby:2.2',
+ services: ['postgres:9.1', 'mysql:5.5'],
+ stage: 'test',
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ variables: { VAR: 'value' },
+ after_script: ['make clean'] },
spinach: { name: :spinach,
+ before_script: [],
script: %w[spinach],
- stage: 'test' }
+ commands: 'spinach',
+ image: 'ruby:2.2',
+ services: ['postgres:9.1', 'mysql:5.5'],
+ stage: 'test',
+ cache: { key: 'k', untracked: true, paths: ['public/'] },
+ variables: {},
+ after_script: ['make clean'] },
)
end
end
@@ -148,17 +168,20 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when most of entires not defined' do
- let(:hash) { { cache: { key: 'a' }, rspec: { script: %w[ls] } } }
- before { global.process! }
+ before { global.compose! }
+
+ let(:hash) do
+ { cache: { key: 'a' }, rspec: { script: %w[ls] } }
+ end
describe '#nodes' do
it 'instantizes all nodes' do
expect(global.descendants.count).to eq 8
end
- it 'contains undefined nodes' do
+ it 'contains unspecified nodes' do
expect(global.descendants.first)
- .to be_an_instance_of Gitlab::Ci::Config::Node::Undefined
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Unspecified
end
end
@@ -188,8 +211,11 @@ describe Gitlab::Ci::Config::Node::Global do
# details.
#
context 'when entires specified but not defined' do
- let(:hash) { { variables: nil, rspec: { script: 'rspec' } } }
- before { global.process! }
+ before { global.compose! }
+
+ let(:hash) do
+ { variables: nil, rspec: { script: 'rspec' } }
+ end
describe '#variables' do
it 'undefined entry returns a default value' do
@@ -200,7 +226,7 @@ describe Gitlab::Ci::Config::Node::Global do
end
context 'when hash is not valid' do
- before { global.process! }
+ before { global.compose! }
let(:hash) do
{ before_script: 'ls' }
@@ -247,4 +273,27 @@ describe Gitlab::Ci::Config::Node::Global do
expect(global.specified?).to be true
end
end
+
+ describe '#[]' do
+ before { global.compose! }
+
+ let(:hash) do
+ { cache: { key: 'a' }, rspec: { script: 'ls' } }
+ end
+
+ context 'when node exists' do
+ it 'returns correct entry' do
+ expect(global[:cache])
+ .to be_an_instance_of Gitlab::Ci::Config::Node::Cache
+ expect(global[:jobs][:rspec][:script].value).to eq ['ls']
+ end
+ end
+
+ context 'when node does not exist' do
+ it 'always return unspecified node' do
+ expect(global[:some][:unknown][:node])
+ .not_to be_specified
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/ci/config/node/job_spec.rb b/spec/lib/gitlab/ci/config/node/job_spec.rb
index 1484fb60dd8..91f676dae03 100644
--- a/spec/lib/gitlab/ci/config/node/job_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/job_spec.rb
@@ -3,9 +3,9 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Job do
let(:entry) { described_class.new(config, name: :rspec) }
- before { entry.process! }
-
describe 'validations' do
+ before { entry.compose! }
+
context 'when entry config value is correct' do
let(:config) { { script: 'rspec' } }
@@ -59,28 +59,82 @@ describe Gitlab::Ci::Config::Node::Job do
end
end
- describe '#value' do
- context 'when entry is correct' do
+ describe '#relevant?' do
+ it 'is a relevant entry' do
+ expect(entry).to be_relevant
+ end
+ end
+
+ describe '#compose!' do
+ let(:unspecified) { double('unspecified', 'specified?' => false) }
+
+ let(:specified) do
+ double('specified', 'specified?' => true, value: 'specified')
+ end
+
+ let(:deps) { double('deps', '[]' => unspecified) }
+
+ context 'when job config overrides global config' do
+ before { entry.compose!(deps) }
+
let(:config) do
- { before_script: %w[ls pwd],
- script: 'rspec',
- after_script: %w[cleanup] }
+ { image: 'some_image', cache: { key: 'test' } }
+ end
+
+ it 'overrides global config' do
+ expect(entry[:image].value).to eq 'some_image'
+ expect(entry[:cache].value).to eq(key: 'test')
+ end
+ end
+
+ context 'when job config does not override global config' do
+ before do
+ allow(deps).to receive('[]').with(:image).and_return(specified)
+ entry.compose!(deps)
end
- it 'returns correct value' do
- expect(entry.value)
- .to eq(name: :rspec,
- before_script: %w[ls pwd],
- script: %w[rspec],
- stage: 'test',
- after_script: %w[cleanup])
+ let(:config) { { script: 'ls', cache: { key: 'test' } } }
+
+ it 'uses config from global entry' do
+ expect(entry[:image].value).to eq 'specified'
+ expect(entry[:cache].value).to eq(key: 'test')
end
end
end
- describe '#relevant?' do
- it 'is a relevant entry' do
- expect(entry).to be_relevant
+ context 'when composed' do
+ before { entry.compose! }
+
+ describe '#value' do
+ before { entry.compose! }
+
+ context 'when entry is correct' do
+ let(:config) do
+ { before_script: %w[ls pwd],
+ script: 'rspec',
+ after_script: %w[cleanup] }
+ end
+
+ it 'returns correct value' do
+ expect(entry.value)
+ .to eq(name: :rspec,
+ before_script: %w[ls pwd],
+ script: %w[rspec],
+ commands: "ls\npwd\nrspec",
+ stage: 'test',
+ after_script: %w[cleanup])
+ end
+ end
+ end
+
+ describe '#commands' do
+ let(:config) do
+ { before_script: %w[ls pwd], script: 'rspec' }
+ end
+
+ it 'returns a string of commands concatenated with new line character' do
+ expect(entry.commands).to eq "ls\npwd\nrspec"
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/jobs_spec.rb b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
index ae2c88aac37..929809339ef 100644
--- a/spec/lib/gitlab/ci/config/node/jobs_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/jobs_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Ci::Config::Node::Jobs do
let(:entry) { described_class.new(config) }
describe 'validations' do
- before { entry.process! }
+ before { entry.compose! }
context 'when entry config value is correct' do
let(:config) { { rspec: { script: 'rspec' } } }
@@ -47,8 +47,8 @@ describe Gitlab::Ci::Config::Node::Jobs do
end
end
- context 'when valid job entries processed' do
- before { entry.process! }
+ context 'when valid job entries composed' do
+ before { entry.compose! }
let(:config) do
{ rspec: { script: 'rspec' },
@@ -61,9 +61,11 @@ describe Gitlab::Ci::Config::Node::Jobs do
expect(entry.value).to eq(
rspec: { name: :rspec,
script: %w[rspec],
+ commands: 'rspec',
stage: 'test' },
spinach: { name: :spinach,
script: %w[spinach],
+ commands: 'spinach',
stage: 'test' })
end
end
diff --git a/spec/lib/gitlab/ci/config/node/null_spec.rb b/spec/lib/gitlab/ci/config/node/null_spec.rb
deleted file mode 100644
index 1ab5478dcfa..00000000000
--- a/spec/lib/gitlab/ci/config/node/null_spec.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::Ci::Config::Node::Null do
- let(:null) { described_class.new(nil) }
-
- describe '#leaf?' do
- it 'is leaf node' do
- expect(null).to be_leaf
- end
- end
-
- describe '#valid?' do
- it 'is always valid' do
- expect(null).to be_valid
- end
- end
-
- describe '#errors' do
- it 'is does not contain errors' do
- expect(null.errors).to be_empty
- end
- end
-
- describe '#value' do
- it 'returns nil' do
- expect(null.value).to eq nil
- end
- end
-
- describe '#relevant?' do
- it 'is not relevant' do
- expect(null.relevant?).to eq false
- end
- end
-
- describe '#specified?' do
- it 'is not defined' do
- expect(null.specified?).to eq false
- end
- end
-end
diff --git a/spec/lib/gitlab/ci/config/node/script_spec.rb b/spec/lib/gitlab/ci/config/node/script_spec.rb
index ee7395362a9..219a7e981d3 100644
--- a/spec/lib/gitlab/ci/config/node/script_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/script_spec.rb
@@ -3,9 +3,7 @@ require 'spec_helper'
describe Gitlab::Ci::Config::Node::Script do
let(:entry) { described_class.new(config) }
- describe '#process!' do
- before { entry.process! }
-
+ describe 'validations' do
context 'when entry config value is correct' do
let(:config) { ['ls', 'pwd'] }
diff --git a/spec/lib/gitlab/ci/config/node/undefined_spec.rb b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
index 2d43e1c1a9d..6bde8602963 100644
--- a/spec/lib/gitlab/ci/config/node/undefined_spec.rb
+++ b/spec/lib/gitlab/ci/config/node/undefined_spec.rb
@@ -1,32 +1,41 @@
require 'spec_helper'
describe Gitlab::Ci::Config::Node::Undefined do
- let(:undefined) { described_class.new(entry) }
- let(:entry) { spy('Entry') }
+ let(:entry) { described_class.new }
+
+ describe '#leaf?' do
+ it 'is leaf node' do
+ expect(entry).to be_leaf
+ end
+ end
describe '#valid?' do
- it 'delegates method to entry' do
- expect(undefined.valid).to eq entry
+ it 'is always valid' do
+ expect(entry).to be_valid
end
end
describe '#errors' do
- it 'delegates method to entry' do
- expect(undefined.errors).to eq entry
+ it 'is does not contain errors' do
+ expect(entry.errors).to be_empty
end
end
describe '#value' do
- it 'delegates method to entry' do
- expect(undefined.value).to eq entry
+ it 'returns nil' do
+ expect(entry.value).to eq nil
end
end
- describe '#specified?' do
- it 'is always false' do
- allow(entry).to receive(:specified?).and_return(true)
+ describe '#relevant?' do
+ it 'is not relevant' do
+ expect(entry.relevant?).to eq false
+ end
+ end
- expect(undefined.specified?).to be false
+ describe '#specified?' do
+ it 'is not defined' do
+ expect(entry.specified?).to eq false
end
end
end
diff --git a/spec/lib/gitlab/ci/config/node/unspecified_spec.rb b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
new file mode 100644
index 00000000000..ba3ceef24ce
--- /dev/null
+++ b/spec/lib/gitlab/ci/config/node/unspecified_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Config::Node::Unspecified do
+ let(:unspecified) { described_class.new(entry) }
+ let(:entry) { spy('Entry') }
+
+ describe '#valid?' do
+ it 'delegates method to entry' do
+ expect(unspecified.valid?).to eq entry
+ end
+ end
+
+ describe '#errors' do
+ it 'delegates method to entry' do
+ expect(unspecified.errors).to eq entry
+ end
+ end
+
+ describe '#value' do
+ it 'delegates method to entry' do
+ expect(unspecified.value).to eq entry
+ end
+ end
+
+ describe '#specified?' do
+ it 'is always false' do
+ allow(entry).to receive(:specified?).and_return(true)
+
+ expect(unspecified.specified?).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline_duration_spec.rb b/spec/lib/gitlab/ci/pipeline_duration_spec.rb
new file mode 100644
index 00000000000..b26728a843c
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline_duration_spec.rb
@@ -0,0 +1,115 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::PipelineDuration do
+ let(:calculated_duration) { calculate(data) }
+
+ shared_examples 'calculating duration' do
+ it do
+ expect(calculated_duration).to eq(duration)
+ end
+ end
+
+ context 'test sample A' do
+ let(:data) do
+ [[0, 1],
+ [1, 2],
+ [3, 4],
+ [5, 6]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample B' do
+ let(:data) do
+ [[0, 1],
+ [1, 2],
+ [2, 3],
+ [3, 4],
+ [0, 4]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample C' do
+ let(:data) do
+ [[0, 4],
+ [2, 6],
+ [5, 7],
+ [8, 9]]
+ end
+
+ let(:duration) { 8 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample D' do
+ let(:data) do
+ [[0, 1],
+ [2, 3],
+ [4, 5],
+ [6, 7]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample E' do
+ let(:data) do
+ [[0, 1],
+ [3, 9],
+ [3, 4],
+ [3, 5],
+ [3, 8],
+ [4, 5],
+ [4, 7],
+ [5, 8]]
+ end
+
+ let(:duration) { 7 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample F' do
+ let(:data) do
+ [[1, 3],
+ [2, 4],
+ [2, 4],
+ [2, 4],
+ [5, 8]]
+ end
+
+ let(:duration) { 6 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ context 'test sample G' do
+ let(:data) do
+ [[1, 3],
+ [2, 4],
+ [6, 7]]
+ end
+
+ let(:duration) { 4 }
+
+ it_behaves_like 'calculating duration'
+ end
+
+ def calculate(data)
+ periods = data.shuffle.map do |(first, last)|
+ Gitlab::Ci::PipelineDuration::Period.new(first, last)
+ end
+
+ Gitlab::Ci::PipelineDuration.from_periods(periods.sort_by(&:first))
+ end
+end
diff --git a/spec/lib/gitlab/conflict/parser_spec.rb b/spec/lib/gitlab/conflict/parser_spec.rb
index a1d2ca1e272..16eb3766356 100644
--- a/spec/lib/gitlab/conflict/parser_spec.rb
+++ b/spec/lib/gitlab/conflict/parser_spec.rb
@@ -179,8 +179,8 @@ CONFLICT
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
- it 'raises UnmergeableFile when the file is over 100 KB' do
- expect { parse_text('a' * 102401) }.
+ it 'raises UnmergeableFile when the file is over 200 KB' do
+ expect { parse_text('a' * 204801) }.
to raise_error(Gitlab::Conflict::Parser::UnmergeableFile)
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 3fb8de81545..7df288f619f 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -13,7 +13,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
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
+ let(:label1) do
double(
name: 'Bug',
color: 'ff0000',
@@ -21,6 +21,14 @@ describe Gitlab::GithubImport::Importer, lib: true do
)
end
+ let(:label2) do
+ double(
+ name: nil,
+ color: 'ff0000',
+ url: 'https://api.github.com/repos/octocat/Hello-World/labels/bug'
+ )
+ end
+
let(:milestone) do
double(
number: 1347,
@@ -93,7 +101,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
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(:labels).and_return([label1, label2])
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])
@@ -113,7 +121,7 @@ describe Gitlab::GithubImport::Importer, lib: true 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: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
{ 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)" },
diff --git a/spec/lib/gitlab/github_import/label_formatter_spec.rb b/spec/lib/gitlab/github_import/label_formatter_spec.rb
index 87593e32db0..8098754d735 100644
--- a/spec/lib/gitlab/github_import/label_formatter_spec.rb
+++ b/spec/lib/gitlab/github_import/label_formatter_spec.rb
@@ -1,18 +1,34 @@
require 'spec_helper'
describe Gitlab::GithubImport::LabelFormatter, lib: true do
- describe '#attributes' do
- it 'returns formatted attributes' do
- project = create(:project)
- raw = double(name: 'improvements', color: 'e6e6e6')
+ let(:project) { create(:project) }
+ let(:raw) { double(name: 'improvements', color: 'e6e6e6') }
- formatter = described_class.new(project, raw)
+ subject { described_class.new(project, raw) }
- expect(formatter.attributes).to eq({
+ describe '#attributes' do
+ it 'returns formatted attributes' do
+ expect(subject.attributes).to eq({
project: project,
title: 'improvements',
color: '#e6e6e6'
})
end
end
+
+ describe '#create!' do
+ context 'when label does not exist' do
+ it 'creates a new label' do
+ expect { subject.create! }.to change(Label, :count).by(1)
+ end
+ end
+
+ context 'when label exists' do
+ it 'does not create a new label' do
+ project.labels.create(name: raw.name)
+
+ expect { subject.create! }.not_to change(Label, :count)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/gitlab_import/importer_spec.rb b/spec/lib/gitlab/gitlab_import/importer_spec.rb
index d3f1deb3837..9b499b593d3 100644
--- a/spec/lib/gitlab/gitlab_import/importer_spec.rb
+++ b/spec/lib/gitlab/gitlab_import/importer_spec.rb
@@ -13,6 +13,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
'title' => 'Issue',
'description' => 'Lorem ipsum',
'state' => 'opened',
+ 'confidential' => true,
'author' => {
'id' => 283999,
'name' => 'John Doe'
@@ -34,6 +35,7 @@ describe Gitlab::GitlabImport::Importer, lib: true do
title: 'Issue',
description: "*Created by: John Doe*\n\nLorem ipsum",
state: 'opened',
+ confidential: true,
author_id: project.creator_id
}
diff --git a/spec/lib/gitlab/popen_spec.rb b/spec/lib/gitlab/popen_spec.rb
index e8b236426e9..4ae216d55b0 100644
--- a/spec/lib/gitlab/popen_spec.rb
+++ b/spec/lib/gitlab/popen_spec.rb
@@ -40,4 +40,13 @@ describe 'Gitlab::Popen', lib: true, no_db: true do
it { expect(@status).to be_zero }
it { expect(@output).to include('spec') }
end
+
+ context 'use stdin' do
+ before do
+ @output, @status = @klass.new.popen(%w[cat]) { |stdin| stdin.write 'hello' }
+ end
+
+ it { expect(@status).to be_zero }
+ it { expect(@output).to eq('hello') }
+ end
end
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 8a656ab0ee9..dfbefad6367 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -12,12 +12,6 @@ describe Gitlab::SearchResults do
let!(:milestone) { create(:milestone, project: project, title: 'foo') }
let(:results) { described_class.new(user, Project.all, 'foo') }
- describe '#total_count' do
- it 'returns the total amount of search hits' do
- expect(results.total_count).to eq(4)
- end
- end
-
describe '#projects_count' do
it 'returns the total amount of projects' do
expect(results.projects_count).to eq(1)
@@ -42,18 +36,6 @@ describe Gitlab::SearchResults do
end
end
- describe '#empty?' do
- it 'returns true when there are no search results' do
- allow(results).to receive(:total_count).and_return(0)
-
- expect(results.empty?).to eq(true)
- end
-
- it 'returns false when there are search results' do
- expect(results.empty?).to eq(false)
- end
- end
-
describe 'confidential issues' do
let(:project_1) { create(:empty_project) }
let(:project_2) { create(:empty_project) }
diff --git a/spec/lib/gitlab/snippet_search_results_spec.rb b/spec/lib/gitlab/snippet_search_results_spec.rb
index e86b9ef6a63..b661a894c0c 100644
--- a/spec/lib/gitlab/snippet_search_results_spec.rb
+++ b/spec/lib/gitlab/snippet_search_results_spec.rb
@@ -5,12 +5,6 @@ describe Gitlab::SnippetSearchResults do
let(:results) { described_class.new(Snippet.all, 'foo') }
- describe '#total_count' do
- it 'returns the total amount of search hits' do
- expect(results.total_count).to eq(2)
- end
- end
-
describe '#snippet_titles_count' do
it 'returns the amount of matched snippet titles' do
expect(results.snippet_titles_count).to eq(1)
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index c5c1402e8fc..395192149a9 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -4,7 +4,7 @@ describe Gitlab::Workhorse, lib: true do
let(:project) { create(:project) }
let(:subject) { Gitlab::Workhorse }
- describe "#send_git_archive" do
+ describe ".send_git_archive" do
context "when the repository doesn't have an archive file path" do
before do
allow(project.repository).to receive(:archive_metadata).and_return(Hash.new)
@@ -15,4 +15,88 @@ describe Gitlab::Workhorse, lib: true do
end
end
end
+
+ describe ".secret" do
+ subject { described_class.secret }
+
+ before do
+ described_class.instance_variable_set(:@secret, nil)
+ described_class.write_secret
+ end
+
+ it 'returns 32 bytes' do
+ expect(subject).to be_a(String)
+ expect(subject.length).to eq(32)
+ expect(subject.encoding).to eq(Encoding::ASCII_8BIT)
+ end
+
+ it 'raises an exception if the secret file cannot be read' do
+ File.delete(described_class.secret_path)
+ expect { subject }.to raise_exception(Errno::ENOENT)
+ end
+
+ it 'raises an exception if the secret file contains the wrong number of bytes' do
+ File.truncate(described_class.secret_path, 0)
+ expect { subject }.to raise_exception(RuntimeError)
+ end
+ end
+
+ describe ".write_secret" do
+ let(:secret_path) { described_class.secret_path }
+ before do
+ begin
+ File.delete(secret_path)
+ rescue Errno::ENOENT
+ end
+
+ described_class.write_secret
+ end
+
+ it 'uses mode 0600' do
+ expect(File.stat(secret_path).mode & 0777).to eq(0600)
+ end
+
+ it 'writes base64 data' do
+ bytes = Base64.strict_decode64(File.read(secret_path))
+ expect(bytes).not_to be_empty
+ end
+ end
+
+ describe '#verify_api_request!' do
+ let(:header_key) { described_class::INTERNAL_API_REQUEST_HEADER }
+ let(:payload) { { 'iss' => 'gitlab-workhorse' } }
+
+ it 'accepts a correct header' do
+ headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
+ expect { call_verify(headers) }.not_to raise_error
+ end
+
+ it 'raises an error when the header is not set' do
+ expect { call_verify({}) }.to raise_jwt_error
+ end
+
+ it 'raises an error when the header is not signed' do
+ headers = { header_key => JWT.encode(payload, nil, 'none') }
+ expect { call_verify(headers) }.to raise_jwt_error
+ end
+
+ it 'raises an error when the header is signed with the wrong key' do
+ headers = { header_key => JWT.encode(payload, 'wrongkey', 'HS256') }
+ expect { call_verify(headers) }.to raise_jwt_error
+ end
+
+ it 'raises an error when the issuer is incorrect' do
+ payload['iss'] = 'somebody else'
+ headers = { header_key => JWT.encode(payload, described_class.secret, 'HS256') }
+ expect { call_verify(headers) }.to raise_jwt_error
+ end
+
+ def raise_jwt_error
+ raise_error(JWT::DecodeError)
+ end
+
+ def call_verify(headers)
+ described_class.verify_api_request!(headers)
+ end
+ end
end
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 36d10636ae9..bce18b4e99e 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -19,4 +19,64 @@ describe Ci::Build, models: true do
expect(build.trace).to eq(test_trace)
end
end
+
+ describe '#has_trace_file?' do
+ context 'when there is no trace' do
+ it { expect(build.has_trace_file?).to be_falsey }
+ it { expect(build.trace).to be_nil }
+ end
+
+ context 'when there is a trace' do
+ context 'when trace is stored in file' do
+ let(:build_with_trace) { create(:ci_build, :trace) }
+
+ it { expect(build_with_trace.has_trace_file?).to be_truthy }
+ it { expect(build_with_trace.trace).to eq('BUILD TRACE') }
+ end
+
+ context 'when trace is stored in old file' do
+ before do
+ allow(build.project).to receive(:ci_id).and_return(999)
+ allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
+ allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(true)
+ allow(File).to receive(:read).with(build.old_path_to_trace).and_return(test_trace)
+ end
+
+ it { expect(build.has_trace_file?).to be_truthy }
+ it { expect(build.trace).to eq(test_trace) }
+ end
+
+ context 'when trace is stored in DB' do
+ before do
+ allow(build.project).to receive(:ci_id).and_return(nil)
+ allow(build).to receive(:read_attribute).with(:trace).and_return(test_trace)
+ allow(File).to receive(:exist?).with(build.path_to_trace).and_return(false)
+ allow(File).to receive(:exist?).with(build.old_path_to_trace).and_return(false)
+ end
+
+ it { expect(build.has_trace_file?).to be_falsey }
+ it { expect(build.trace).to eq(test_trace) }
+ end
+ end
+ end
+
+ describe '#trace_file_path' do
+ context 'when trace is stored in file' do
+ before do
+ allow(build).to receive(:has_trace_file?).and_return(true)
+ allow(build).to receive(:has_old_trace_file?).and_return(false)
+ end
+
+ it { expect(build.trace_file_path).to eq(build.path_to_trace) }
+ end
+
+ context 'when trace is stored in old file' do
+ before do
+ allow(build).to receive(:has_trace_file?).and_return(true)
+ allow(build).to receive(:has_old_trace_file?).and_return(true)
+ end
+
+ it { expect(build.trace_file_path).to eq(build.old_path_to_trace) }
+ end
+ end
end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 598df576001..fbf945c757c 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -124,21 +124,38 @@ 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 }
+ let(:build) { create_build('build1', current, 10) }
+ let(:build_b) { create_build('build2', current, 20) }
+ let(:build_c) { create_build('build3', current + 50, 10) }
describe '#duration' do
before do
- travel_to(current - 120) do
+ pipeline.update(created_at: current)
+
+ travel_to(current + 5) do
pipeline.run
+ pipeline.save
+ end
+
+ travel_to(current + 30) do
+ build.success
+ end
+
+ travel_to(current + 40) do
+ build_b.drop
end
- travel_to(current) do
- pipeline.succeed
+ travel_to(current + 70) do
+ build_c.success
end
+
+ pipeline.drop
end
it 'matches sum of builds duration' do
- expect(pipeline.reload.duration).to eq(120)
+ pipeline.reload
+
+ expect(pipeline.duration).to eq(40)
end
end
@@ -169,6 +186,14 @@ describe Ci::Pipeline, models: true do
expect(pipeline.reload.finished_at).to be_nil
end
end
+
+ def create_build(name, queued_at = current, started_from = 0)
+ create(:ci_build,
+ name: name,
+ pipeline: pipeline,
+ queued_at: queued_at,
+ started_at: queued_at + started_from)
+ end
end
describe '#branch?' do
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 6a640474cfe..3db5937a4f3 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -31,6 +31,43 @@ describe DiffNote, models: true do
subject { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
+ describe ".resolve!" do
+ let(:current_user) { create(:user) }
+ let!(:commit_note) { create(:diff_note_on_commit) }
+ let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
+ let!(:unresolved_note) { create(:diff_note_on_merge_request) }
+
+ before do
+ described_class.resolve!(current_user)
+
+ commit_note.reload
+ resolved_note.reload
+ unresolved_note.reload
+ end
+
+ it 'resolves only the resolvable, not yet resolved notes' do
+ expect(commit_note.resolved_at).to be_nil
+ expect(resolved_note.resolved_by).not_to eq(current_user)
+ expect(unresolved_note.resolved_at).not_to be_nil
+ expect(unresolved_note.resolved_by).to eq(current_user)
+ end
+ end
+
+ describe ".unresolve!" do
+ let!(:resolved_note) { create(:diff_note_on_merge_request, :resolved) }
+
+ before do
+ described_class.unresolve!
+
+ resolved_note.reload
+ end
+
+ it 'unresolves the resolved notes' do
+ expect(resolved_note.resolved_by).to be_nil
+ expect(resolved_note.resolved_at).to be_nil
+ end
+ end
+
describe "#position=" do
context "when provided a string" do
it "sets the position" do
diff --git a/spec/models/discussion_spec.rb b/spec/models/discussion_spec.rb
index 179f2e73662..0142706d140 100644
--- a/spec/models/discussion_spec.rb
+++ b/spec/models/discussion_spec.rb
@@ -238,27 +238,19 @@ describe Discussion, model: true do
context "when resolvable" do
let(:user) { create(:user) }
+ let(:second_note) { create(:diff_note_on_commit) } # unresolvable
before do
allow(subject).to receive(:resolvable?).and_return(true)
-
- allow(first_note).to receive(:resolvable?).and_return(true)
- allow(second_note).to receive(:resolvable?).and_return(false)
- allow(third_note).to receive(:resolvable?).and_return(true)
end
context "when all resolvable notes are resolved" do
before do
first_note.resolve!(user)
third_note.resolve!(user)
- end
- it "calls resolve! on every resolvable note" do
- expect(first_note).to receive(:resolve!).with(current_user)
- expect(second_note).not_to receive(:resolve!)
- expect(third_note).to receive(:resolve!).with(current_user)
-
- subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
end
it "doesn't change resolved_at on the resolved notes" do
@@ -309,46 +301,44 @@ describe Discussion, model: true do
first_note.resolve!(user)
end
- it "calls resolve! on every resolvable note" do
- expect(first_note).to receive(:resolve!).with(current_user)
- expect(second_note).not_to receive(:resolve!)
- expect(third_note).to receive(:resolve!).with(current_user)
-
- subject.resolve!(current_user)
- end
-
it "doesn't change resolved_at on the resolved note" do
expect(first_note.resolved_at).not_to be_nil
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_at }
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload.resolved_at }
end
it "doesn't change resolved_by on the resolved note" do
expect(first_note.resolved_by).to eq(user)
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved_by }
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload && first_note.resolved_by }
end
it "doesn't change the resolved state on the resolved note" do
expect(first_note.resolved?).to be true
- expect { subject.resolve!(current_user) }.not_to change { first_note.resolved? }
+ expect { subject.resolve!(current_user) }.
+ not_to change { first_note.reload && first_note.resolved? }
end
it "sets resolved_at on the unresolved note" do
subject.resolve!(current_user)
+ third_note.reload
expect(third_note.resolved_at).not_to be_nil
end
it "sets resolved_by on the unresolved note" do
subject.resolve!(current_user)
+ third_note.reload
expect(third_note.resolved_by).to eq(current_user)
end
it "marks the unresolved note as resolved" do
subject.resolve!(current_user)
+ third_note.reload
expect(third_note.resolved?).to be true
end
@@ -373,16 +363,10 @@ describe Discussion, model: true do
end
context "when no resolvable notes are resolved" do
- it "calls resolve! on every resolvable note" do
- expect(first_note).to receive(:resolve!).with(current_user)
- expect(second_note).not_to receive(:resolve!)
- expect(third_note).to receive(:resolve!).with(current_user)
-
- subject.resolve!(current_user)
- end
-
it "sets resolved_at on the unresolved notes" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(first_note.resolved_at).not_to be_nil
expect(third_note.resolved_at).not_to be_nil
@@ -390,6 +374,8 @@ describe Discussion, model: true do
it "sets resolved_by on the unresolved notes" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(first_note.resolved_by).to eq(current_user)
expect(third_note.resolved_by).to eq(current_user)
@@ -397,6 +383,8 @@ describe Discussion, model: true do
it "marks the unresolved notes as resolved" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(first_note.resolved?).to be true
expect(third_note.resolved?).to be true
@@ -404,18 +392,24 @@ describe Discussion, model: true do
it "sets resolved_at" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(subject.resolved_at).not_to be_nil
end
it "sets resolved_by" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(subject.resolved_by).to eq(current_user)
end
it "marks as resolved" do
subject.resolve!(current_user)
+ first_note.reload
+ third_note.reload
expect(subject.resolved?).to be true
end
@@ -451,16 +445,10 @@ describe Discussion, model: true do
third_note.resolve!(user)
end
- it "calls unresolve! on every resolvable note" do
- expect(first_note).to receive(:unresolve!)
- expect(second_note).not_to receive(:unresolve!)
- expect(third_note).to receive(:unresolve!)
-
- subject.unresolve!
- end
-
it "unsets resolved_at on the resolved notes" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(first_note.resolved_at).to be_nil
expect(third_note.resolved_at).to be_nil
@@ -468,6 +456,8 @@ describe Discussion, model: true do
it "unsets resolved_by on the resolved notes" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(first_note.resolved_by).to be_nil
expect(third_note.resolved_by).to be_nil
@@ -475,6 +465,8 @@ describe Discussion, model: true do
it "unmarks the resolved notes as resolved" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(first_note.resolved?).to be false
expect(third_note.resolved?).to be false
@@ -482,12 +474,16 @@ describe Discussion, model: true do
it "unsets resolved_at" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(subject.resolved_at).to be_nil
end
it "unsets resolved_by" do
subject.unresolve!
+ first_note.reload
+ third_note.reload
expect(subject.resolved_by).to be_nil
end
@@ -504,40 +500,22 @@ describe Discussion, model: true do
first_note.resolve!(user)
end
- it "calls unresolve! on every resolvable note" do
- expect(first_note).to receive(:unresolve!)
- expect(second_note).not_to receive(:unresolve!)
- expect(third_note).to receive(:unresolve!)
-
- subject.unresolve!
- end
-
it "unsets resolved_at on the resolved note" do
subject.unresolve!
- expect(first_note.resolved_at).to be_nil
+ expect(subject.first_note.resolved_at).to be_nil
end
it "unsets resolved_by on the resolved note" do
subject.unresolve!
- expect(first_note.resolved_by).to be_nil
+ expect(subject.first_note.resolved_by).to be_nil
end
it "unmarks the resolved note as resolved" do
subject.unresolve!
- expect(first_note.resolved?).to be false
- end
- end
-
- context "when no resolvable notes are resolved" do
- it "calls unresolve! on every resolvable note" do
- expect(first_note).to receive(:unresolve!)
- expect(second_note).not_to receive(:unresolve!)
- expect(third_note).to receive(:unresolve!)
-
- subject.unresolve!
+ expect(subject.first_note.resolved?).to be false
end
end
end
diff --git a/spec/models/project_services/slack_service/build_message_spec.rb b/spec/models/project_services/slack_service/build_message_spec.rb
index 7fcfdf0eacd..452f4e2782c 100644
--- a/spec/models/project_services/slack_service/build_message_spec.rb
+++ b/spec/models/project_services/slack_service/build_message_spec.rb
@@ -10,7 +10,7 @@ describe SlackService::BuildMessage do
tag: false,
project_name: 'project_name',
- project_url: 'somewhere.com',
+ project_url: 'example.gitlab.com',
commit: {
status: status,
@@ -20,42 +20,38 @@ describe SlackService::BuildMessage do
}
end
- context 'succeeded' do
+ let(:message) { build_message }
+
+ context 'build succeeded' do
let(:status) { 'success' }
let(:color) { 'good' }
let(:duration) { 10 }
-
+ let(:message) { build_message('passed') }
+
it 'returns a message with information about succeeded build' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker passed in 10 seconds'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
end
- context 'failed' do
+ context 'build failed' do
let(:status) { 'failed' }
let(:color) { 'danger' }
let(:duration) { 10 }
it 'returns a message with information about failed build' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 10 seconds'
expect(subject.pretext).to be_empty
expect(subject.fallback).to eq(message)
expect(subject.attachments).to eq([text: message, color: color])
end
- end
-
- describe '#seconds_name' do
- let(:status) { 'failed' }
- let(:color) { 'danger' }
- let(:duration) { 1 }
+ end
- it 'returns seconds as singular when there is only one' do
- message = '<somewhere.com|project_name>: Commit <somewhere.com/commit/97de212e80737a608d939f648d959671fb0a0142/builds|97de212e> of <somewhere.com/commits/develop|develop> branch by hacker failed in 1 second'
- expect(subject.pretext).to be_empty
- expect(subject.fallback).to eq(message)
- expect(subject.attachments).to eq([text: message, color: color])
- end
+ def build_message(status_text = status)
+ "<example.gitlab.com|project_name>:" \
+ " Commit <example.gitlab.com/commit/" \
+ "97de212e80737a608d939f648d959671fb0a0142/builds|97de212e>" \
+ " of <example.gitlab.com/commits/develop|develop> branch" \
+ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
end
end
diff --git a/spec/models/project_services/slack_service/pipeline_message_spec.rb b/spec/models/project_services/slack_service/pipeline_message_spec.rb
new file mode 100644
index 00000000000..babb3909f56
--- /dev/null
+++ b/spec/models/project_services/slack_service/pipeline_message_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe SlackService::PipelineMessage do
+ subject { SlackService::PipelineMessage.new(args) }
+
+ let(:args) do
+ {
+ object_attributes: {
+ id: 123,
+ sha: '97de212e80737a608d939f648d959671fb0a0142',
+ tag: false,
+ ref: 'develop',
+ status: status,
+ duration: duration
+ },
+ project: { path_with_namespace: 'project_name',
+ web_url: 'example.gitlab.com' },
+ commit: { author_name: 'hacker' }
+ }
+ end
+
+ let(:message) { build_message }
+
+ context 'pipeline succeeded' do
+ let(:status) { 'success' }
+ let(:color) { 'good' }
+ let(:duration) { 10 }
+ let(:message) { build_message('passed') }
+
+ it 'returns a message with information about succeeded build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ context 'pipeline failed' do
+ let(:status) { 'failed' }
+ let(:color) { 'danger' }
+ let(:duration) { 10 }
+
+ it 'returns a message with information about failed build' do
+ expect(subject.pretext).to be_empty
+ expect(subject.fallback).to eq(message)
+ expect(subject.attachments).to eq([text: message, color: color])
+ end
+ end
+
+ def build_message(status_text = status)
+ "<example.gitlab.com|project_name>:" \
+ " Pipeline <example.gitlab.com/pipelines/123|97de212e>" \
+ " of <example.gitlab.com/commits/develop|develop> branch" \
+ " by hacker #{status_text} in #{duration} #{'second'.pluralize(duration)}"
+ end
+end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index 28af68d13b4..5afdc4b2f7b 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -21,6 +21,9 @@
require 'spec_helper'
describe SlackService, models: true do
+ let(:slack) { SlackService.new }
+ let(:webhook_url) { 'https://example.gitlab.com/' }
+
describe "Associations" do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
@@ -42,15 +45,14 @@ describe SlackService, models: true do
end
describe "Execute" do
- let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project) }
+ let(:username) { 'slack_username' }
+ let(:channel) { 'slack_channel' }
+
let(:push_sample_data) do
Gitlab::DataBuilder::Push.build_sample(project, user)
end
- let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
- let(:username) { 'slack_username' }
- let(:channel) { 'slack_channel' }
before do
allow(slack).to receive_messages(
@@ -212,10 +214,8 @@ describe SlackService, models: true do
end
describe "Note events" do
- let(:slack) { SlackService.new }
let(:user) { create(:user) }
let(:project) { create(:project, creator_id: user.id) }
- let(:webhook_url) { 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685' }
before do
allow(slack).to receive_messages(
@@ -285,4 +285,63 @@ describe SlackService, models: true do
end
end
end
+
+ describe 'Pipeline events' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline,
+ project: project, status: status,
+ sha: project.commit.sha, ref: project.default_branch)
+ end
+
+ before do
+ allow(slack).to receive_messages(
+ project: project,
+ service_hook: true,
+ webhook: webhook_url
+ )
+ end
+
+ shared_examples 'call Slack API' do
+ before do
+ WebMock.stub_request(:post, webhook_url)
+ end
+
+ it 'calls Slack API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ slack.execute(data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+ end
+
+ context 'with failed pipeline' do
+ let(:status) { 'failed' }
+
+ it_behaves_like 'call Slack API'
+ end
+
+ context 'with succeeded pipeline' do
+ let(:status) { 'success' }
+
+ context 'with default to notify_only_broken_pipelines' do
+ it 'does not call Slack API for pipeline events' do
+ data = Gitlab::DataBuilder::Pipeline.build(pipeline)
+ result = slack.execute(data)
+
+ expect(result).to be_falsy
+ end
+ end
+
+ context 'with setting notify_only_broken_pipelines to false' do
+ before do
+ slack.notify_only_broken_pipelines = false
+ end
+
+ it_behaves_like 'call Slack API'
+ end
+ end
+ end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 812c72c48cb..7624050878e 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -441,43 +441,77 @@ describe Repository, models: true do
end
end
- describe '#commit_with_hooks' do
+ describe '#update_branch_with_hooks' do
let(:old_rev) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' } # git rev-parse feature
+ let(:new_rev) { 'a74ae73c1ccde9b974a70e82b901588071dc142a' } # commit whose parent is old_rev
context 'when pre hooks were successful' do
before do
expect_any_instance_of(GitHooksService).to receive(:execute).
- with(user, repository.path_to_repo, old_rev, sample_commit.id, 'refs/heads/feature').
+ with(user, repository.path_to_repo, old_rev, new_rev, 'refs/heads/feature').
and_yield.and_return(true)
end
it 'runs without errors' do
expect do
- repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ repository.update_branch_with_hooks(user, 'feature') { new_rev }
end.not_to raise_error
end
it 'ensures the autocrlf Git option is set to :input' do
expect(repository).to receive(:update_autocrlf_option)
- repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ repository.update_branch_with_hooks(user, 'feature') { new_rev }
end
context "when the branch wasn't empty" do
it 'updates the head' do
expect(repository.find_branch('feature').target.id).to eq(old_rev)
- repository.commit_with_hooks(user, 'feature') { sample_commit.id }
- expect(repository.find_branch('feature').target.id).to eq(sample_commit.id)
+ repository.update_branch_with_hooks(user, 'feature') { new_rev }
+ expect(repository.find_branch('feature').target.id).to eq(new_rev)
end
end
end
+ context 'when the update adds more than one commit' do
+ it 'runs without errors' do
+ old_rev = '33f3729a45c02fc67d00adb1b8bca394b0e761d9'
+
+ # old_rev is an ancestor of new_rev
+ expect(repository.rugged.merge_base(old_rev, new_rev)).to eq(old_rev)
+
+ # old_rev is not a direct ancestor (parent) of new_rev
+ expect(repository.rugged.lookup(new_rev).parent_ids).not_to include(old_rev)
+
+ branch = 'feature-ff-target'
+ repository.add_branch(user, branch, old_rev)
+
+ expect { repository.update_branch_with_hooks(user, branch) { new_rev } }.not_to raise_error
+ end
+ end
+
+ context 'when the update would remove commits from the target branch' do
+ it 'raises an exception' do
+ branch = 'master'
+ old_rev = repository.find_branch(branch).target.sha
+
+ # The 'master' branch is NOT an ancestor of new_rev.
+ expect(repository.rugged.merge_base(old_rev, new_rev)).not_to eq(old_rev)
+
+ # Updating 'master' to new_rev would lose the commits on 'master' that
+ # are not contained in new_rev. This should not be allowed.
+ expect do
+ repository.update_branch_with_hooks(user, branch) { new_rev }
+ end.to raise_error(Repository::CommitError)
+ end
+ end
+
context 'when pre hooks failed' do
it 'gets an error' do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([false, ''])
expect do
- repository.commit_with_hooks(user, 'feature') { sample_commit.id }
+ repository.update_branch_with_hooks(user, 'feature') { new_rev }
end.to raise_error(GitHooksService::PreReceiveError)
end
end
@@ -485,6 +519,7 @@ describe Repository, models: true do
context 'when target branch is different from source branch' do
before do
allow_any_instance_of(Gitlab::Git::Hook).to receive(:trigger).and_return([true, ''])
+ allow(repository).to receive(:update_ref!)
end
it 'expires branch cache' do
@@ -495,7 +530,7 @@ describe Repository, models: true do
expect(repository).to receive(:expire_has_visible_content_cache)
expect(repository).to receive(:expire_branch_count_cache)
- repository.commit_with_hooks(user, 'new-feature') { sample_commit.id }
+ repository.update_branch_with_hooks(user, 'new-feature') { new_rev }
end
end
@@ -1268,4 +1303,18 @@ describe Repository, models: true do
File.delete(path)
end
end
+
+ describe '#update_ref!' do
+ it 'can create a ref' do
+ repository.update_ref!('refs/heads/foobar', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+
+ expect(repository.find_branch('foobar')).not_to be_nil
+ end
+
+ it 'raises CommitError when the ref update fails' do
+ expect do
+ repository.update_ref!('refs/heads/master', 'refs/heads/master', Gitlab::Git::BLANK_SHA)
+ end.to raise_error(Repository::CommitError)
+ end
+ end
end
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index f802fcd2d2e..06e3a2183c0 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -6,6 +6,12 @@ describe API::API, api: true do
let(:user2) { create(:user) }
let(:user3) { create(:user) }
let(:admin) { create(:admin) }
+ let(:group) { create(:group) }
+ let(:group2) do
+ group = create(:group, name: 'group2_name')
+ group.add_owner(user2)
+ group
+ end
let(:project) do
create(:project, creator_id: user.id, namespace: user.namespace)
@@ -22,6 +28,7 @@ describe API::API, api: true do
context 'when authenticated' do
it 'forks if user has sufficient access to project' do
post api("/projects/fork/#{project.id}", user2)
+
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
@@ -32,6 +39,7 @@ describe API::API, api: true do
it 'forks if user is admin' do
post api("/projects/fork/#{project.id}", admin)
+
expect(response).to have_http_status(201)
expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path)
@@ -42,12 +50,14 @@ describe API::API, api: true do
it 'fails on missing project access for the project to fork' do
post api("/projects/fork/#{project.id}", user3)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'fails if forked project exists in the user namespace' do
post api("/projects/fork/#{project.id}", user)
+
expect(response).to have_http_status(409)
expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken'])
@@ -55,14 +65,70 @@ describe API::API, api: true do
it 'fails if project to fork from does not exist' do
post api('/projects/fork/424242', user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Project Not Found')
end
+
+ it 'forks with explicit own user namespace id' do
+ post api("/projects/fork/#{project.id}", user2), namespace: user2.namespace.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'forks with explicit own user name as namespace' do
+ post api("/projects/fork/#{project.id}", user2), namespace: user2.username
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'forks to another user when admin' do
+ post api("/projects/fork/#{project.id}", admin), namespace: user2.username
+
+ expect(response).to have_http_status(201)
+ expect(json_response['owner']['id']).to eq(user2.id)
+ end
+
+ it 'fails if trying to fork to another user when not admin' do
+ post api("/projects/fork/#{project.id}", user2), namespace: admin.namespace.id
+
+ expect(response).to have_http_status(409)
+ end
+
+ it 'fails if trying to fork to non-existent namespace' do
+ post api("/projects/fork/#{project.id}", user2), namespace: 42424242
+
+ expect(response).to have_http_status(404)
+ expect(json_response['message']).to eq('404 Target Namespace Not Found')
+ end
+
+ it 'forks to owned group' do
+ post api("/projects/fork/#{project.id}", user2), namespace: group2.name
+
+ expect(response).to have_http_status(201)
+ expect(json_response['namespace']['name']).to eq(group2.name)
+ end
+
+ it 'fails to fork to not owned group' do
+ post api("/projects/fork/#{project.id}", user2), namespace: group.name
+
+ expect(response).to have_http_status(409)
+ end
+
+ it 'forks to not owned group when admin' do
+ post api("/projects/fork/#{project.id}", admin), namespace: group.name
+
+ expect(response).to have_http_status(201)
+ expect(json_response['namespace']['name']).to eq(group.name)
+ end
end
context 'when unauthenticated' do
it 'returns authentication error' do
post api("/projects/fork/#{project.id}")
+
expect(response).to have_http_status(401)
expect(json_response['message']).to eq('401 Unauthorized')
end
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 5d06abcfeb3..46d1b868782 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -44,8 +44,8 @@ describe API::API, api: true do
secret_token: secret_token,
key_id: 12345
- expect(response).to have_http_status(404)
- expect(json_response['message']).to eq('404 Not found')
+ expect(json_response['success']).to be_falsey
+ expect(json_response['message']).to eq('Could not find the given key')
end
it 'returns an error message when the key is a deploy key' do
diff --git a/spec/requests/api/lint_spec.rb b/spec/requests/api/lint_spec.rb
new file mode 100644
index 00000000000..391fc13a380
--- /dev/null
+++ b/spec/requests/api/lint_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe API::Lint, api: true do
+ include ApiHelpers
+
+ describe 'POST /ci/lint' do
+ context 'with valid .gitlab-ci.yaml content' do
+ let(:yaml_content) do
+ File.read(Rails.root.join('spec/support/gitlab_stubs/gitlab_ci.yml'))
+ end
+
+ it 'passes validation' do
+ post api('/ci/lint'), { content: yaml_content }
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Hash
+ expect(json_response['status']).to eq('valid')
+ expect(json_response['errors']).to eq([])
+ end
+ end
+
+ context 'with an invalid .gitlab_ci.yml' do
+ it 'responds with errors about invalid syntax' do
+ post api('/ci/lint'), { content: 'invalid content' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq('invalid')
+ expect(json_response['errors']).to eq(['Invalid configuration format'])
+ end
+
+ it "responds with errors about invalid configuration" do
+ post api('/ci/lint'), { content: '{ image: "ruby:2.1", services: ["postgres"] }' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['status']).to eq('invalid')
+ expect(json_response['errors']).to eq(['jobs config should contain at least one visible job'])
+ end
+ end
+
+ context 'without the content parameter' do
+ it 'responds with validation error about missing content' do
+ post api('/ci/lint')
+
+ expect(response).to have_http_status(400)
+ expect(json_response['error']).to eq('content is missing')
+ end
+ end
+ end
+end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index ca7932dc5da..9e390bea50b 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -230,7 +230,8 @@ describe Ci::API::API do
let(:post_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:delete_url) { ci_api("/builds/#{build.id}/artifacts") }
let(:get_url) { ci_api("/builds/#{build.id}/artifacts") }
- let(:headers) { { "GitLab-Workhorse" => "1.0" } }
+ let(:jwt_token) { JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256') }
+ let(:headers) { { "GitLab-Workhorse" => "1.0", Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER => jwt_token } }
let(:headers_with_token) { headers.merge(Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token) }
before { build.run! }
@@ -240,14 +241,22 @@ describe Ci::API::API do
it "using token as parameter" do
post authorize_url, { token: build.token }, headers
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
end
it "using token as header" do
post authorize_url, {}, headers_with_token
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
expect(json_response["TempPath"]).not_to be_nil
end
+
+ it "reject requests that did not go through gitlab-workhorse" do
+ headers.delete(Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER)
+ post authorize_url, { token: build.token }, headers
+ expect(response).to have_http_status(500)
+ end
end
context "should fail to post too large artifact" do
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 9ca3b021aa2..b7001fede40 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,6 +1,8 @@
require "spec_helper"
describe 'Git HTTP requests', lib: true do
+ include WorkhorseHelpers
+
let(:user) { create(:user) }
let(:project) { create(:project, path: 'project.git-project') }
@@ -48,6 +50,7 @@ describe 'Git HTTP requests', lib: true do
expect(response).to have_http_status(200)
expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
end
@@ -63,6 +66,7 @@ describe 'Git HTTP requests', lib: true do
it "downloads get status 200" do
download(path, {}) do |response|
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
@@ -101,6 +105,14 @@ describe 'Git HTTP requests', lib: true do
end
end
end
+
+ context 'when the request is not from gitlab-workhorse' do
+ it 'raises an exception' do
+ expect do
+ get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack")
+ end.to raise_error(JWT::DecodeError)
+ end
+ end
end
context "when the project is private" do
@@ -170,11 +182,13 @@ describe 'Git HTTP requests', lib: true do
clone_get(path, env)
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it "uploads get status 200" do
upload(path, env) do |response|
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
end
@@ -189,6 +203,7 @@ describe 'Git HTTP requests', lib: true do
clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it "uploads get status 401 (no project existence information leak)" do
@@ -297,6 +312,7 @@ describe 'Git HTTP requests', lib: true do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
it "uploads get status 401 (no project existence information leak)" do
@@ -426,7 +442,7 @@ describe 'Git HTTP requests', lib: true do
end
def auth_env(user, password, spnego_request_token)
- env = {}
+ env = workhorse_internal_api_request_header
if user && password
env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
elsif spnego_request_token
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index fcd6521317a..6e551bb65fa 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe 'Git LFS API and storage' do
+ include WorkhorseHelpers
+
let(:user) { create(:user) }
let!(:lfs_object) { create(:lfs_object, :with_file) }
@@ -715,6 +717,12 @@ describe 'Git LFS API and storage' do
project.team << [user, :developer]
end
+ context 'and the request bypassed workhorse' do
+ it 'raises an exception' do
+ expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
+ end
+ end
+
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
@@ -724,6 +732,10 @@ describe 'Git LFS API and storage' do
expect(response).to have_http_status(200)
end
+ it 'uses the gitlab-workhorse content type' do
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+
it 'responds with status 200, location of lfs store and object details' do
expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
expect(json_response['LfsOid']).to eq(sample_oid)
@@ -863,8 +875,11 @@ describe 'Git LFS API and storage' do
end
end
- def put_authorize
- put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, headers
+ def put_authorize(verified: true)
+ authorize_headers = headers
+ authorize_headers.merge!(workhorse_internal_api_request_header) if verified
+
+ put "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize", nil, authorize_headers
end
def put_finalize(lfs_tmp = lfs_tmp_file)
diff --git a/spec/services/issues/bulk_update_service_spec.rb b/spec/services/issuable/bulk_update_service_spec.rb
index ac08aa53b0b..6f7ce8ca992 100644
--- a/spec/services/issues/bulk_update_service_spec.rb
+++ b/spec/services/issuable/bulk_update_service_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
-describe Issues::BulkUpdateService, services: true do
+describe Issuable::BulkUpdateService, services: true do
let(:user) { create(:user) }
let(:project) { create(:empty_project, namespace: user.namespace) }
def bulk_update(issues, extra_params = {})
bulk_update_params = extra_params
- .reverse_merge(issues_ids: Array(issues).map(&:id).join(','))
+ .reverse_merge(issuable_ids: Array(issues).map(&:id).join(','))
- Issues::BulkUpdateService.new(project, user, bulk_update_params).execute
+ Issuable::BulkUpdateService.new(project, user, bulk_update_params).execute('issue')
end
describe 'close issues' do
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index aff022a573e..5dfb33f4b28 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -18,12 +18,12 @@ describe Issues::CloseService, services: true do
context "valid params" do
before do
perform_enqueued_jobs do
- @issue = described_class.new(project, user, {}).execute(issue)
+ described_class.new(project, user).execute(issue)
end
end
- it { expect(@issue).to be_valid }
- it { expect(@issue).to be_closed }
+ it { expect(issue).to be_valid }
+ it { expect(issue).to be_closed }
it 'sends email to user2 about assign of new issue' do
email = ActionMailer::Base.deliveries.last
@@ -32,7 +32,7 @@ describe Issues::CloseService, services: true do
end
it 'creates system note about issue reassign' do
- note = @issue.notes.last
+ note = issue.notes.last
expect(note.note).to include "Status changed to closed"
end
@@ -44,23 +44,43 @@ describe Issues::CloseService, services: true do
context 'current user is not authorized to close issue' do
before do
perform_enqueued_jobs do
- @issue = described_class.new(project, guest).execute(issue)
+ described_class.new(project, guest).execute(issue)
end
end
it 'does not close the issue' do
- expect(@issue).to be_open
+ expect(issue).to be_open
end
end
- context "external issue tracker" do
+ context 'when issue is not confidential' do
+ it 'executes issue hooks' do
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+ described_class.new(project, user).execute(issue)
+ end
+ end
+
+ context 'when issue is confidential' do
+ it 'executes confidential issue hooks' do
+ issue = create(:issue, :confidential, project: project)
+
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+ described_class.new(project, user).execute(issue)
+ end
+ end
+
+ context 'external issue tracker' do
before do
allow(project).to receive(:default_issues_tracker?).and_return(false)
- @issue = described_class.new(project, user, {}).execute(issue)
+ described_class.new(project, user).execute(issue)
end
- it { expect(@issue).to be_valid }
- it { expect(@issue).to be_opened }
+ it { expect(issue).to be_valid }
+ it { expect(issue).to be_opened }
it { expect(todo.reload).to be_pending }
end
end
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index fcc3c0a00bd..58569ba96c3 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -72,6 +72,24 @@ describe Issues::CreateService, services: true do
expect(issue.milestone).not_to eq milestone
end
end
+
+ it 'executes issue hooks when issue is not confidential' do
+ opts = { title: 'Title', description: 'Description', confidential: false }
+
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+ described_class.new(project, user, opts).execute
+ end
+
+ it 'executes confidential issue hooks when issue is confidential' do
+ opts = { title: 'Title', description: 'Description', confidential: true }
+
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+ described_class.new(project, user, opts).execute
+ end
end
it_behaves_like 'new issuable record that supports slash commands'
diff --git a/spec/services/issues/reopen_service_spec.rb b/spec/services/issues/reopen_service_spec.rb
index 34a89fcd4e1..93a8270fd16 100644
--- a/spec/services/issues/reopen_service_spec.rb
+++ b/spec/services/issues/reopen_service_spec.rb
@@ -1,24 +1,50 @@
require 'spec_helper'
describe Issues::ReopenService, services: true do
- let(:guest) { create(:user) }
- let(:issue) { create(:issue, :closed) }
- let(:project) { issue.project }
-
- before do
- project.team << [guest, :guest]
- end
+ let(:project) { create(:empty_project) }
+ let(:issue) { create(:issue, :closed, project: project) }
describe '#execute' do
- context 'current user is not authorized to reopen issue' do
+ context 'when user is not authorized to reopen issue' do
before do
+ guest = create(:user)
+ project.team << [guest, :guest]
+
perform_enqueued_jobs do
- @issue = described_class.new(project, guest).execute(issue)
+ described_class.new(project, guest).execute(issue)
end
end
it 'does not reopen the issue' do
- expect(@issue).to be_closed
+ expect(issue).to be_closed
+ end
+ end
+
+ context 'when user is authrized to reopen issue' do
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ context 'when issue is not confidential' do
+ it 'executes issue hooks' do
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :issue_hooks)
+
+ described_class.new(project, user).execute(issue)
+ end
+ end
+
+ context 'when issue is confidential' do
+ it 'executes confidential issue hooks' do
+ issue = create(:issue, :confidential, :closed, project: project)
+
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+ described_class.new(project, user).execute(issue)
+ end
end
end
end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 0313f424463..4f5375a3583 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -23,11 +23,15 @@ describe Issues::UpdateService, services: true do
describe 'execute' do
def find_note(starting_with)
- @issue.notes.find do |note|
+ issue.notes.find do |note|
note && note.note.start_with?(starting_with)
end
end
+ def update_issue(opts)
+ described_class.new(project, user, opts).execute(issue)
+ end
+
context "valid params" do
before do
opts = {
@@ -35,23 +39,20 @@ describe Issues::UpdateService, services: true do
description: 'Also please fix',
assignee_id: user2.id,
state_event: 'close',
- label_ids: [label.id],
- confidential: true
+ label_ids: [label.id]
}
perform_enqueued_jobs do
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ update_issue(opts)
end
-
- @issue.reload
end
- it { expect(@issue).to be_valid }
- it { expect(@issue.title).to eq('New title') }
- it { expect(@issue.assignee).to eq(user2) }
- it { expect(@issue).to be_closed }
- it { expect(@issue.labels.count).to eq(1) }
- it { expect(@issue.labels.first.title).to eq(label.name) }
+ it { expect(issue).to be_valid }
+ it { expect(issue.title).to eq('New title') }
+ it { expect(issue.assignee).to eq(user2) }
+ it { expect(issue).to be_closed }
+ it { expect(issue.labels.count).to eq(1) }
+ it { expect(issue.labels.first.title).to eq(label.name) }
it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do
deliveries = ActionMailer::Base.deliveries
@@ -81,18 +82,35 @@ describe Issues::UpdateService, services: true do
expect(note).not_to be_nil
expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
end
+ end
+
+ context 'when issue turns confidential' do
+ let(:opts) do
+ {
+ title: 'New title',
+ description: 'Also please fix',
+ assignee_id: user2.id,
+ state_event: 'close',
+ label_ids: [label.id],
+ confidential: true
+ }
+ end
it 'creates system note about confidentiality change' do
+ update_issue(confidential: true)
+
note = find_note('Made the issue confidential')
expect(note).not_to be_nil
expect(note.note).to eq 'Made the issue confidential'
end
- end
- def update_issue(opts)
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
- @issue.reload
+ it 'executes confidential issue hooks' do
+ expect(project).to receive(:execute_hooks).with(an_instance_of(Hash), :confidential_issue_hooks)
+ expect(project).to receive(:execute_services).with(an_instance_of(Hash), :confidential_issue_hooks)
+
+ update_issue(confidential: true)
+ end
end
context 'todos' do
@@ -100,7 +118,7 @@ describe Issues::UpdateService, services: true do
context 'when the title change' do
before do
- update_issue({ title: 'New title' })
+ update_issue(title: 'New title')
end
it 'marks pending todos as done' do
@@ -110,7 +128,7 @@ describe Issues::UpdateService, services: true do
context 'when the description change' do
before do
- update_issue({ description: 'Also please fix' })
+ update_issue(description: 'Also please fix')
end
it 'marks todos as done' do
@@ -120,7 +138,7 @@ describe Issues::UpdateService, services: true do
context 'when is reassigned' do
before do
- update_issue({ assignee: user2 })
+ update_issue(assignee: user2)
end
it 'marks previous assignee todos as done' do
@@ -144,7 +162,7 @@ describe Issues::UpdateService, services: true do
context 'when the milestone change' do
before do
- update_issue({ milestone: create(:milestone) })
+ update_issue(milestone: create(:milestone))
end
it 'marks todos as done' do
@@ -154,7 +172,7 @@ describe Issues::UpdateService, services: true do
context 'when the labels change' do
before do
- update_issue({ label_ids: [label.id] })
+ update_issue(label_ids: [label.id])
end
it 'marks todos as done' do
@@ -165,6 +183,7 @@ describe Issues::UpdateService, services: true do
context 'when the issue is relabeled' do
let!(:non_subscriber) { create(:user) }
+
let!(:subscriber) do
create(:user).tap do |u|
label.toggle_subscription(u)
@@ -176,7 +195,7 @@ describe Issues::UpdateService, services: true do
opts = { label_ids: [label.id] }
perform_enqueued_jobs do
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue = described_class.new(project, user, opts).execute(issue)
end
should_email(subscriber)
@@ -190,7 +209,7 @@ describe Issues::UpdateService, services: true do
opts = { label_ids: [label.id, label2.id] }
perform_enqueued_jobs do
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue = described_class.new(project, user, opts).execute(issue)
end
should_not_email(subscriber)
@@ -201,7 +220,7 @@ describe Issues::UpdateService, services: true do
opts = { label_ids: [label2.id] }
perform_enqueued_jobs do
- @issue = Issues::UpdateService.new(project, user, opts).execute(issue)
+ @issue = described_class.new(project, user, opts).execute(issue)
end
should_not_email(subscriber)
@@ -210,13 +229,15 @@ describe Issues::UpdateService, services: true do
end
end
- context 'when Issue has tasks' do
- before { update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" }) }
+ context 'when issue has tasks' do
+ before do
+ update_issue(description: "- [ ] Task 1\n- [ ] Task 2")
+ end
- it { expect(@issue.tasks?).to eq(true) }
+ it { expect(issue.tasks?).to eq(true) }
context 'when tasks are marked as completed' do
- before { update_issue({ description: "- [x] Task 1\n- [X] Task 2" }) }
+ before { update_issue(description: "- [x] Task 1\n- [X] Task 2") }
it 'creates system note about task status change' do
note1 = find_note('Marked the task **Task 1** as completed')
@@ -229,8 +250,8 @@ describe Issues::UpdateService, services: true do
context 'when tasks are marked as incomplete' do
before do
- update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
- update_issue({ description: "- [ ] Task 1\n- [ ] Task 2" })
+ update_issue(description: "- [x] Task 1\n- [X] Task 2")
+ update_issue(description: "- [ ] Task 1\n- [ ] Task 2")
end
it 'creates system note about task status change' do
@@ -244,8 +265,8 @@ describe Issues::UpdateService, services: true do
context 'when tasks position has been modified' do
before do
- update_issue({ description: "- [x] Task 1\n- [X] Task 2" })
- update_issue({ description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2" })
+ update_issue(description: "- [x] Task 1\n- [X] Task 2")
+ update_issue(description: "- [x] Task 1\n- [ ] Task 3\n- [ ] Task 2")
end
it 'does not create a system note' do
@@ -257,8 +278,8 @@ describe Issues::UpdateService, services: true do
context 'when a Task list with a completed item is totally replaced' do
before do
- update_issue({ description: "- [ ] Task 1\n- [X] Task 2" })
- update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+ update_issue(description: "- [ ] Task 1\n- [X] Task 2")
+ update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three")
end
it 'does not create a system note referencing the position the old item' do
@@ -269,7 +290,7 @@ describe Issues::UpdateService, services: true do
it 'does not generate a new note at all' do
expect do
- update_issue({ description: "- [ ] One\n- [ ] Two\n- [ ] Three" })
+ update_issue(description: "- [ ] One\n- [ ] Two\n- [ ] Three")
end.not_to change { Note.count }
end
end
@@ -277,7 +298,7 @@ describe Issues::UpdateService, services: true do
context 'updating labels' do
let(:label3) { create(:label, project: project) }
- let(:result) { Issues::UpdateService.new(project, user, params).execute(issue).reload }
+ let(:result) { described_class.new(project, user, params).execute(issue).reload }
context 'when add_label_ids and label_ids are passed' do
let(:params) { { label_ids: [label.id], add_label_ids: [label3.id] } }
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 232508cda23..0d586e2216b 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -99,14 +99,14 @@ describe MergeRequests::BuildService, services: true do
let(:source_branch) { "#{issue.iid}-fix-issue" }
it 'appends "Closes #$issue-iid" to the description' do
- expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\nCloses ##{issue.iid}")
+ expect(merge_request.description).to eq("#{commit_1.safe_message.split(/\n+/, 2).last}\n\nCloses ##{issue.iid}")
end
context 'merge request already has a description set' do
let(:description) { 'Merge request description' }
it 'appends "Closes #$issue-iid" to the description' do
- expect(merge_request.description).to eq("#{description}\nCloses ##{issue.iid}")
+ expect(merge_request.description).to eq("#{description}\n\nCloses ##{issue.iid}")
end
end
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index cafcad3e3c0..b41f6f14fbd 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -145,6 +145,14 @@ describe TodoService, services: true do
end
end
+ describe '#destroy_issue' do
+ it 'refresh the todos count cache for the user' do
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.destroy_issue(issue, john_doe)
+ end
+ end
+
describe '#reassigned_issue' do
it 'creates a pending todo for new assignee' do
unassigned_issue.update_attribute(:assignee, john_doe)
@@ -394,6 +402,14 @@ describe TodoService, services: true do
end
end
+ describe '#destroy_merge_request' do
+ it 'refresh the todos count cache for the user' do
+ expect(john_doe).to receive(:update_todos_count_cache).and_call_original
+
+ service.destroy_merge_request(mr_assigned, john_doe)
+ end
+ end
+
describe '#reassigned_merge_request' do
it 'creates a pending todo for new assignee' do
mr_unassigned.update_attribute(:assignee, john_doe)
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index e0dbc9aa84c..ac38e31b77e 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -15,7 +15,7 @@ RSpec.configure do |config|
DatabaseCleaner.start
end
- config.after(:each) do
+ config.append_after(:each) do
DatabaseCleaner.clean
end
end
diff --git a/spec/support/issuable_slash_commands_shared_examples.rb b/spec/support/issuable_slash_commands_shared_examples.rb
index d2a49ea5c5e..5e3b8f2b23e 100644
--- a/spec/support/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/issuable_slash_commands_shared_examples.rb
@@ -2,6 +2,9 @@
# It takes a `issuable_type`, and expect an `issuable`.
shared_examples 'issuable record that supports slash commands in its description and notes' do |issuable_type|
+ include SlashCommandsHelpers
+ include WaitForAjax
+
let(:master) { create(:user) }
let(:assignee) { create(:user, username: 'bob') }
let(:guest) { create(:user) }
@@ -18,6 +21,11 @@ shared_examples 'issuable record that supports slash commands in its description
login_with(master)
end
+ after do
+ # Ensure all outstanding Ajax requests are complete to avoid database deadlocks
+ wait_for_ajax
+ end
+
describe "new #{issuable_type}" do
context 'with commands in the description' do
it "creates the #{issuable_type} and interpret commands accordingly" do
@@ -44,10 +52,7 @@ shared_examples 'issuable record that supports slash commands in its description
context 'with a note containing commands' do
it 'creates a note without the commands and interpret the commands accordingly' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
- click_button 'Comment'
- end
+ write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
expect(page).to have_content 'Awesome!'
expect(page).not_to have_content '/assign @bob'
@@ -66,10 +71,7 @@ shared_examples 'issuable record that supports slash commands in its description
context 'with a note containing only commands' do
it 'does not create a note but interpret the commands accordingly' do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/assign @bob\n/label ~bug\n/milestone %\"ASAP\""
- click_button 'Comment'
- end
+ write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
expect(page).not_to have_content '/assign @bob'
expect(page).not_to have_content '/label ~bug'
@@ -92,10 +94,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "when current user can close #{issuable_type}" do
it "closes the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/close"
- click_button 'Comment'
- end
+ write_note("/close")
expect(page).not_to have_content '/close'
expect(page).to have_content 'Your commands have been executed!'
@@ -112,10 +111,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not close the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/close"
- click_button 'Comment'
- end
+ write_note("/close")
expect(page).not_to have_content '/close'
expect(page).not_to have_content 'Your commands have been executed!'
@@ -133,10 +129,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "when current user can reopen #{issuable_type}" do
it "reopens the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/reopen"
- click_button 'Comment'
- end
+ write_note("/reopen")
expect(page).not_to have_content '/reopen'
expect(page).to have_content 'Your commands have been executed!'
@@ -153,10 +146,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not reopen the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/reopen"
- click_button 'Comment'
- end
+ write_note("/reopen")
expect(page).not_to have_content '/reopen'
expect(page).not_to have_content 'Your commands have been executed!'
@@ -169,10 +159,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note changing the #{issuable_type}'s title" do
context "when current user can change title of #{issuable_type}" do
it "reopens the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/title Awesome new title"
- click_button 'Comment'
- end
+ write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
expect(page).to have_content 'Your commands have been executed!'
@@ -189,10 +176,7 @@ shared_examples 'issuable record that supports slash commands in its description
end
it "does not reopen the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/title Awesome new title"
- click_button 'Comment'
- end
+ write_note("/title Awesome new title")
expect(page).not_to have_content '/title'
expect(page).not_to have_content 'Your commands have been executed!'
@@ -204,10 +188,7 @@ shared_examples 'issuable record that supports slash commands in its description
context "with a note marking the #{issuable_type} as todo" do
it "creates a new todo for the #{issuable_type}" do
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/todo"
- click_button 'Comment'
- end
+ write_note("/todo")
expect(page).not_to have_content '/todo'
expect(page).to have_content 'Your commands have been executed!'
@@ -238,10 +219,7 @@ shared_examples 'issuable record that supports slash commands in its description
expect(todo.author).to eq master
expect(todo.user).to eq master
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/done"
- click_button 'Comment'
- end
+ write_note("/done")
expect(page).not_to have_content '/done'
expect(page).to have_content 'Your commands have been executed!'
@@ -254,10 +232,7 @@ shared_examples 'issuable record that supports slash commands in its description
it "creates a new todo for the #{issuable_type}" do
expect(issuable.subscribed?(master)).to be_falsy
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/subscribe"
- click_button 'Comment'
- end
+ write_note("/subscribe")
expect(page).not_to have_content '/subscribe'
expect(page).to have_content 'Your commands have been executed!'
@@ -274,10 +249,7 @@ shared_examples 'issuable record that supports slash commands in its description
it "creates a new todo for the #{issuable_type}" do
expect(issuable.subscribed?(master)).to be_truthy
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "/unsubscribe"
- click_button 'Comment'
- end
+ write_note("/unsubscribe")
expect(page).not_to have_content '/unsubscribe'
expect(page).to have_content 'Your commands have been executed!'
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index e5f76afbfc0..c0b3e83244d 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -75,6 +75,7 @@ module LoginHelpers
def logout
find(".header-user-dropdown-toggle").click
click_link "Sign out"
+ expect(page).to have_content('Signed out successfully')
end
# Logout without JavaScript driver
diff --git a/spec/support/slash_commands_helpers.rb b/spec/support/slash_commands_helpers.rb
new file mode 100644
index 00000000000..df483afa0e3
--- /dev/null
+++ b/spec/support/slash_commands_helpers.rb
@@ -0,0 +1,10 @@
+module SlashCommandsHelpers
+ def write_note(text)
+ Sidekiq::Testing.fake! do
+ page.within('.js-main-target-form') do
+ fill_in 'note[note]', with: text
+ click_button 'Comment'
+ end
+ end
+ end
+end
diff --git a/spec/support/workhorse_helpers.rb b/spec/support/workhorse_helpers.rb
index 107b6e30924..47673cd4c3a 100644
--- a/spec/support/workhorse_helpers.rb
+++ b/spec/support/workhorse_helpers.rb
@@ -13,4 +13,9 @@ module WorkhorseHelpers
]
end
end
+
+ def workhorse_internal_api_request_header
+ jwt_token = JWT.encode({ 'iss' => 'gitlab-workhorse' }, Gitlab::Workhorse.secret, 'HS256')
+ { 'HTTP_' + Gitlab::Workhorse::INTERNAL_API_REQUEST_HEADER.upcase.tr('-', '_') => jwt_token }
+ end
end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index 464051063d8..446ba3bfa14 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -59,14 +59,10 @@ describe 'projects/builds/show' do
end
it 'shows trigger variables in separate lines' do
- expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_1', 'TRIGGER_VALUE_1'))
- expect(rendered).to have_css('code', text: variable_regexp('TRIGGER_KEY_2', 'TRIGGER_VALUE_2'))
+ expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_1')
+ expect(rendered).to have_css('.js-build-variable', visible: false, text: 'TRIGGER_KEY_2')
+ expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_1')
+ expect(rendered).to have_css('.js-build-value', visible: false, text: 'TRIGGER_VALUE_2')
end
end
-
- private
-
- def variable_regexp(key, value)
- /\A#{Regexp.escape("#{key}=#{value}")}\Z/
- end
end
diff --git a/spec/workers/prune_old_events_worker_spec.rb b/spec/workers/prune_old_events_worker_spec.rb
new file mode 100644
index 00000000000..35e1518a35e
--- /dev/null
+++ b/spec/workers/prune_old_events_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe PruneOldEventsWorker do
+ describe '#perform' do
+ let!(:expired_event) { create(:event, author_id: 0, created_at: 13.months.ago) }
+ let!(:not_expired_event) { create(:event, author_id: 0, created_at: 1.day.ago) }
+ let!(:exactly_12_months_event) { create(:event, author_id: 0, created_at: 12.months.ago) }
+
+ it 'prunes events older than 12 months' do
+ expect { subject.perform }.to change { Event.count }.by(-1)
+ expect(Event.find_by(id: expired_event.id)).to be_nil
+ end
+
+ it 'leaves fresh events' do
+ subject.perform
+ expect(not_expired_event.reload).to be_present
+ end
+
+ it 'leaves events from exactly 12 months ago' do
+ subject.perform
+ expect(exactly_12_months_event).to be_present
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/task_list.js b/vendor/assets/javascripts/task_list.js
index bc451506b6a..9fbfef03f6d 100644
--- a/vendor/assets/javascripts/task_list.js
+++ b/vendor/assets/javascripts/task_list.js
@@ -1,15 +1,118 @@
-
+// The MIT License (MIT)
+//
+// Copyright (c) 2014 GitHub, Inc.
+//
+// Permission is hereby granted, free of charge, to any person obtaining a copy
+// of this software and associated documentation files (the "Software"), to deal
+// in the Software without restriction, including without limitation the rights
+// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+// copies of the Software, and to permit persons to whom the Software is
+// furnished to do so, subject to the following conditions:
+//
+// The above copyright notice and this permission notice shall be included in all
+// copies or substantial portions of the Software.
+//
+// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+// SOFTWARE.
+// TaskList Behavior
+//
/*= provides tasklist:enabled */
-
-
/*= provides tasklist:disabled */
-
-
/*= provides tasklist:change */
-
-
/*= provides tasklist:changed */
-
+//
+//
+// Enables Task List update behavior.
+//
+// ### Example Markup
+//
+// <div class="js-task-list-container">
+// <ul class="task-list">
+// <li class="task-list-item">
+// <input type="checkbox" class="js-task-list-item-checkbox" disabled />
+// text
+// </li>
+// </ul>
+// <form>
+// <textarea class="js-task-list-field">- [ ] text</textarea>
+// </form>
+// </div>
+//
+// ### Specification
+//
+// TaskLists MUST be contained in a `(div).js-task-list-container`.
+//
+// TaskList Items SHOULD be an a list (`UL`/`OL`) element.
+//
+// Task list items MUST match `(input).task-list-item-checkbox` and MUST be
+// `disabled` by default.
+//
+// TaskLists MUST have a `(textarea).js-task-list-field` form element whose
+// `value` attribute is the source (Markdown) to be udpated. The source MUST
+// follow the syntax guidelines.
+//
+// TaskList updates trigger `tasklist:change` events. If the change is
+// successful, `tasklist:changed` is fired. The change can be canceled.
+//
+// jQuery is required.
+//
+// ### Methods
+//
+// `.taskList('enable')` or `.taskList()`
+//
+// Enables TaskList updates for the container.
+//
+// `.taskList('disable')`
+//
+// Disables TaskList updates for the container.
+//
+//# ### Events
+//
+// `tasklist:enabled`
+//
+// Fired when the TaskList is enabled.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-container`
+//
+// `tasklist:disabled`
+//
+// Fired when the TaskList is disabled.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-container`
+//
+// `tasklist:change`
+//
+// Fired before the TaskList item change takes affect.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** Yes
+// * **Target** `.js-task-list-field`
+//
+// `tasklist:changed`
+//
+// Fired once the TaskList item change has taken affect.
+//
+// * **Synchronicity** Sync
+// * **Bubbles** Yes
+// * **Cancelable** No
+// * **Target** `.js-task-list-field`
+//
+// ### NOTE
+//
+// Task list checkboxes are rendered as disabled by default because rendered
+// user content is cached without regard for the viewer.
(function() {
var codeFencesPattern, complete, completePattern, disableTaskList, disableTaskLists, enableTaskList, enableTaskLists, escapePattern, incomplete, incompletePattern, itemPattern, itemsInParasPattern, updateTaskList, updateTaskListItem,
indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; };
@@ -18,20 +121,48 @@
complete = "[x]";
+ // Escapes the String for regular expression matching.
escapePattern = function(str) {
return str.replace(/([\[\]])/g, "\\$1").replace(/\s/, "\\s").replace("x", "[xX]");
};
- incompletePattern = RegExp("" + (escapePattern(incomplete)));
-
- completePattern = RegExp("" + (escapePattern(complete)));
+ incompletePattern = RegExp("" + (escapePattern(incomplete))); // escape square brackets
+ // match all white space
+ completePattern = RegExp("" + (escapePattern(complete))); // match all cases
+ // Pattern used to identify all task list items.
+ // Useful when you need iterate over all items.
itemPattern = RegExp("^(?:\\s*(?:>\\s*)*(?:[-+*]|(?:\\d+\\.)))\\s*(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ")\\s+(?!\\(.*?\\))(?=(?:\\[.*?\\]\\s*(?:\\[.*?\\]|\\(.*?\\))\\s*)*(?:[^\\[]|$))");
+ // prefix, consisting of
+ // optional leading whitespace
+ // zero or more blockquotes
+ // list item indicator
+ // optional whitespace prefix
+ // checkbox
+ // is followed by whitespace
+ // is not part of a [foo](url) link
+ // and is followed by zero or more links
+ // and either a non-link or the end of the string
+ // Used to filter out code fences from the source for comparison only.
+ // http://rubular.com/r/x5EwZVrloI
+ // Modified slightly due to issues with JS
codeFencesPattern = /^`{3}(?:\s*\w+)?[\S\s].*[\S\s]^`{3}$/mg;
+ // ```
+ // followed by optional language
+ // whitespace
+ // code
+ // whitespace
+ // ```
+ // Used to filter out potential mismatches (items not in lists).
+ // http://rubular.com/r/OInl6CiePy
itemsInParasPattern = RegExp("^(" + (escapePattern(complete)) + "|" + (escapePattern(incomplete)) + ").+$", "g");
+ // Given the source text, updates the appropriate task list item to match the
+ // given checked value.
+ //
+ // Returns the updated String text.
updateTaskListItem = function(source, itemIndex, checked) {
var clean, index, line, result;
clean = source.replace(/\r/g, '').replace(codeFencesPattern, '').replace(itemsInParasPattern, '').split("\n");
@@ -55,6 +186,9 @@
return result.join("\n");
};
+ // Updates the $field value to reflect the state of $item.
+ // Triggers the `tasklist:change` event before the value has changed, and fires
+ // a `tasklist:changed` event once the value has changed.
updateTaskList = function($item) {
var $container, $field, checked, event, index;
$container = $item.closest('.js-task-list-container');
@@ -70,10 +204,12 @@
}
};
+ // When the task list item checkbox is updated, submit the change
$(document).on('change', '.task-list-item-checkbox', function() {
return updateTaskList($(this));
});
+ // Enables TaskList item changes.
enableTaskList = function($container) {
if ($container.find('.js-task-list-field').length > 0) {
$container.find('.task-list-item').addClass('enabled').find('.task-list-item-checkbox').attr('disabled', null);
@@ -81,6 +217,7 @@
}
};
+ // Enables a collection of TaskList containers.
enableTaskLists = function($containers) {
var container, i, len, results;
results = [];
@@ -91,11 +228,13 @@
return results;
};
+ // Disable TaskList item changes.
disableTaskList = function($container) {
$container.find('.task-list-item').removeClass('enabled').find('.task-list-item-checkbox').attr('disabled', 'disabled');
return $container.removeClass('is-task-list-enabled').trigger('tasklist:disabled');
};
+ // Disables a collection of TaskList containers.
disableTaskLists = function($containers) {
var container, i, len, results;
results = [];