summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLin Jen-Shin <godfat@godfat.org>2016-11-02 16:18:21 +0800
committerLin Jen-Shin <godfat@godfat.org>2016-11-02 16:18:21 +0800
commit80032a84dcaa06774a553863c48f011c3ec53367 (patch)
treec6e4b287cb7f4f213faa7ffa5e915c7d479a6918
parente90a555745fd99673916038ae053879a45462a6f (diff)
parent90072d61e167915a95c26a9af438ef5b6f1f0b03 (diff)
downloadgitlab-ce-80032a84dcaa06774a553863c48f011c3ec53367.tar.gz
Merge remote-tracking branch 'upstream/master' into pipeline-notifications
* upstream/master: (216 commits) Remove invalid changelog entries require rails/generators for generators Adds variable. Fixes changelog Fix haml_lint Fixes after review Update changelog Remove tooltips from project header Makes table pagination responsive 23545 Fix distorted project and group avatars Fix spinach tests Update links in side nav and header Allow to search for user by secondary email address in the admin interface Rename :name search parameter to :search_query at /admin/users Initialize Sidekiq with the list of queues used by GitLab Fix project features default values Add a link to the Issue Boards API in main README Add validation errors to Merge Request form Fix Markdown styling inside reference links Fix relative links in Markdown wiki when displayed in "Project" tab Adds label description to issue board title ...
-rw-r--r--.eslintignore4
-rw-r--r--.eslintrc23
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml28
-rw-r--r--.scss-lint.yml2
-rw-r--r--CHANGELOG.md384
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock16
-rw-r--r--README.md4
-rw-r--r--app/assets/javascripts/abuse_reports.js.es61
-rw-r--r--app/assets/javascripts/activities.js5
-rw-r--r--app/assets/javascripts/admin.js1
-rw-r--r--app/assets/javascripts/api.js1
-rw-r--r--app/assets/javascripts/application.js16
-rw-r--r--app/assets/javascripts/aside.js1
-rw-r--r--app/assets/javascripts/autosave.js1
-rw-r--r--app/assets/javascripts/awards_handler.js12
-rw-r--r--app/assets/javascripts/behaviors/autosize.js1
-rw-r--r--app/assets/javascripts/behaviors/details_behavior.js6
-rw-r--r--app/assets/javascripts/behaviors/quick_submit.js1
-rw-r--r--app/assets/javascripts/behaviors/requires_input.js1
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js1
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es61
-rw-r--r--app/assets/javascripts/blob/blob_file_dropzone.js1
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js1
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selectors.js1
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js1
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.es61
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es68
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js1
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js1
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es614
-rw-r--r--app/assets/javascripts/boards/components/board.js.es622
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es61
-rw-r--r--app/assets/javascripts/boards/components/board_card.js.es635
-rw-r--r--app/assets/javascripts/boards/components/board_delete.js.es61
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es61
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es68
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js.es653
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es62
-rw-r--r--app/assets/javascripts/boards/filters/due_date_filters.js.es65
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es65
-rw-r--r--app/assets/javascripts/boards/models/issue.js.es624
-rw-r--r--app/assets/javascripts/boards/models/label.js.es61
-rw-r--r--app/assets/javascripts/boards/models/list.js.es61
-rw-r--r--app/assets/javascripts/boards/models/milestone.js.es67
-rw-r--r--app/assets/javascripts/boards/models/user.js.es61
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es63
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js.es611
-rw-r--r--app/assets/javascripts/boards/test_utils/simulate_drag.js1
-rw-r--r--app/assets/javascripts/boards/vue_resource_interceptor.js.es61
-rw-r--r--app/assets/javascripts/breakpoints.js1
-rw-r--r--app/assets/javascripts/broadcast_message.js1
-rw-r--r--app/assets/javascripts/build.js1
-rw-r--r--app/assets/javascripts/build_artifacts.js1
-rw-r--r--app/assets/javascripts/build_variables.js.es61
-rw-r--r--app/assets/javascripts/commit.js1
-rw-r--r--app/assets/javascripts/commit/file.js1
-rw-r--r--app/assets/javascripts/commit/image_file.js1
-rw-r--r--app/assets/javascripts/commits.js1
-rw-r--r--app/assets/javascripts/compare.js1
-rw-r--r--app/assets/javascripts/compare_autocomplete.js.es61
-rw-r--r--app/assets/javascripts/confirm_danger_modal.js1
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js1
-rw-r--r--app/assets/javascripts/create_label.js.es61
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6 (renamed from app/assets/javascripts/cycle_analytics.js.es6)7
-rw-r--r--app/assets/javascripts/diff.js1
-rw-r--r--app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_count.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/mixins/discussion.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/models/discussion.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/models/note.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es61
-rw-r--r--app/assets/javascripts/diff_notes/stores/comments.js.es61
-rw-r--r--app/assets/javascripts/dispatcher.js.es61
-rw-r--r--app/assets/javascripts/dropzone_input.js1
-rw-r--r--app/assets/javascripts/due_date_select.js.es630
-rw-r--r--app/assets/javascripts/extensions/array.js1
-rw-r--r--app/assets/javascripts/extensions/element.js.es67
-rw-r--r--app/assets/javascripts/extensions/jquery.js1
-rw-r--r--app/assets/javascripts/files_comment_button.js1
-rw-r--r--app/assets/javascripts/flash.js1
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es61
-rw-r--r--app/assets/javascripts/gl_dropdown.js41
-rw-r--r--app/assets/javascripts/gl_field_errors.js.es61
-rw-r--r--app/assets/javascripts/gl_form.js1
-rw-r--r--app/assets/javascripts/graphs/graphs_bundle.js3
-rw-r--r--app/assets/javascripts/graphs/stat_graph.js1
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js1
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js12
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_util.js1
-rw-r--r--app/assets/javascripts/group_avatar.js1
-rw-r--r--app/assets/javascripts/groups_select.js1
-rw-r--r--app/assets/javascripts/header.js10
-rw-r--r--app/assets/javascripts/importer_status.js1
-rw-r--r--app/assets/javascripts/issuable.js.es657
-rw-r--r--app/assets/javascripts/issuable_context.js1
-rw-r--r--app/assets/javascripts/issuable_form.js5
-rw-r--r--app/assets/javascripts/issue.js7
-rw-r--r--app/assets/javascripts/issue_status_select.js1
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es61
-rw-r--r--app/assets/javascripts/label_manager.js.es61
-rw-r--r--app/assets/javascripts/labels.js1
-rw-r--r--app/assets/javascripts/labels_select.js30
-rw-r--r--app/assets/javascripts/layout_nav.js1
-rw-r--r--app/assets/javascripts/lib/ace.js1
-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.js1
-rw-r--r--app/assets/javascripts/lib/utils/animate.js1
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js9
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js1
-rw-r--r--app/assets/javascripts/lib/utils/jquery.timeago.js1
-rw-r--r--app/assets/javascripts/lib/utils/notify.js1
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js4
-rw-r--r--app/assets/javascripts/lib/utils/type_utility.js1
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js1
-rw-r--r--app/assets/javascripts/line_highlighter.js1
-rw-r--r--app/assets/javascripts/logo.js1
-rw-r--r--app/assets/javascripts/member_expiration_date.js1
-rw-r--r--app/assets/javascripts/members.js.es61
-rw-r--r--app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es61
-rw-r--r--app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es61
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es61
-rw-r--r--app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es61
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es61
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es67
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es61
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es61
-rw-r--r--app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es61
-rw-r--r--app/assets/javascripts/merge_request.js7
-rw-r--r--app/assets/javascripts/merge_request_tabs.js23
-rw-r--r--app/assets/javascripts/merge_request_widget.js.es61
-rw-r--r--app/assets/javascripts/merged_buttons.js1
-rw-r--r--app/assets/javascripts/milestone.js1
-rw-r--r--app/assets/javascripts/milestone_select.js22
-rw-r--r--app/assets/javascripts/namespace_select.js1
-rw-r--r--app/assets/javascripts/network/branch_graph.js1
-rw-r--r--app/assets/javascripts/network/network.js1
-rw-r--r--app/assets/javascripts/network/network_bundle.js3
-rw-r--r--app/assets/javascripts/new_branch_form.js1
-rw-r--r--app/assets/javascripts/new_commit_form.js1
-rw-r--r--app/assets/javascripts/notes.js1
-rw-r--r--app/assets/javascripts/notifications_dropdown.js1
-rw-r--r--app/assets/javascripts/notifications_form.js1
-rw-r--r--app/assets/javascripts/pager.js1
-rw-r--r--app/assets/javascripts/pipelines.js.es648
-rw-r--r--app/assets/javascripts/preview_markdown.js1
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es61
-rw-r--r--app/assets/javascripts/profile/profile.js.es61
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js1
-rw-r--r--app/assets/javascripts/project.js11
-rw-r--r--app/assets/javascripts/project_avatar.js1
-rw-r--r--app/assets/javascripts/project_find_file.js1
-rw-r--r--app/assets/javascripts/project_fork.js1
-rw-r--r--app/assets/javascripts/project_import.js1
-rw-r--r--app/assets/javascripts/project_new.js1
-rw-r--r--app/assets/javascripts/project_select.js1
-rw-r--r--app/assets/javascripts/project_show.js1
-rw-r--r--app/assets/javascripts/projects_list.js1
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es61
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js.es61
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es61
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js.es61
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es61
-rw-r--r--app/assets/javascripts/protected_branches/protected_branches_bundle.js1
-rw-r--r--app/assets/javascripts/right_sidebar.js27
-rw-r--r--app/assets/javascripts/search.js1
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es61
-rw-r--r--app/assets/javascripts/shortcuts.js1
-rw-r--r--app/assets/javascripts/shortcuts_blob.js1
-rw-r--r--app/assets/javascripts/shortcuts_dashboard_navigation.js1
-rw-r--r--app/assets/javascripts/shortcuts_find_file.js1
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js1
-rw-r--r--app/assets/javascripts/shortcuts_navigation.js1
-rw-r--r--app/assets/javascripts/shortcuts_network.js1
-rw-r--r--app/assets/javascripts/sidebar.js.es615
-rw-r--r--app/assets/javascripts/single_file_diff.js20
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js1
-rw-r--r--app/assets/javascripts/snippets_list.js.es61
-rw-r--r--app/assets/javascripts/star.js3
-rw-r--r--app/assets/javascripts/subscription.js33
-rw-r--r--app/assets/javascripts/subscription_select.js1
-rw-r--r--app/assets/javascripts/syntax_highlight.js1
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es613
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es61
-rw-r--r--app/assets/javascripts/todos.js.es64
-rw-r--r--app/assets/javascripts/tree.js1
-rw-r--r--app/assets/javascripts/u2f/authenticate.js1
-rw-r--r--app/assets/javascripts/u2f/error.js1
-rw-r--r--app/assets/javascripts/u2f/register.js1
-rw-r--r--app/assets/javascripts/u2f/util.js1
-rw-r--r--app/assets/javascripts/user.js.es66
-rw-r--r--app/assets/javascripts/user_tabs.js.es61
-rw-r--r--app/assets/javascripts/username_validator.js.es61
-rw-r--r--app/assets/javascripts/users/calendar.js1
-rw-r--r--app/assets/javascripts/users/users_bundle.js1
-rw-r--r--app/assets/javascripts/users_select.js46
-rw-r--r--app/assets/javascripts/wikis.js1
-rw-r--r--app/assets/javascripts/zen_mode.js1
-rw-r--r--app/assets/stylesheets/framework/avatar.scss63
-rw-r--r--app/assets/stylesheets/framework/buttons.scss2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss9
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss87
-rw-r--r--app/assets/stylesheets/framework/header.scss8
-rw-r--r--app/assets/stylesheets/framework/lists.scss8
-rw-r--r--app/assets/stylesheets/framework/pagination.scss62
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss17
-rw-r--r--app/assets/stylesheets/framework/timeline.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss2
-rw-r--r--app/assets/stylesheets/pages/admin.scss9
-rw-r--r--app/assets/stylesheets/pages/awards.scss8
-rw-r--r--app/assets/stylesheets/pages/boards.scss66
-rw-r--r--app/assets/stylesheets/pages/builds.scss26
-rw-r--r--app/assets/stylesheets/pages/commit.scss62
-rw-r--r--app/assets/stylesheets/pages/commits.scss31
-rw-r--r--app/assets/stylesheets/pages/dashboard.scss4
-rw-r--r--app/assets/stylesheets/pages/diff.scss6
-rw-r--r--app/assets/stylesheets/pages/groups.scss9
-rw-r--r--app/assets/stylesheets/pages/login.scss1
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss10
-rw-r--r--app/assets/stylesheets/pages/notes.scss5
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss11
-rw-r--r--app/assets/stylesheets/pages/profile.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss12
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss60
-rw-r--r--app/assets/stylesheets/pages/ui_dev_kit.scss2
-rw-r--r--app/assets/stylesheets/print.scss2
-rw-r--r--app/controllers/admin/users_controller.rb2
-rw-r--r--app/controllers/concerns/service_params.rb2
-rw-r--r--app/controllers/projects/boards/issues_controller.rb9
-rw-r--r--app/controllers/projects/issues_controller.rb2
-rw-r--r--app/controllers/projects/labels_controller.rb2
-rw-r--r--app/controllers/projects/merge_requests_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb17
-rw-r--r--app/controllers/projects/tags_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb5
-rw-r--r--app/finders/issuable_finder.rb4
-rw-r--r--app/finders/labels_finder.rb17
-rw-r--r--app/helpers/button_helper.rb3
-rw-r--r--app/helpers/events_helper.rb6
-rw-r--r--app/helpers/issuables_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb6
-rw-r--r--app/helpers/sidekiq_helper.rb2
-rw-r--r--app/models/ci/pipeline.rb12
-rw-r--r--app/models/commit_status.rb12
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/project_features_compatibility.rb2
-rw-r--r--app/models/concerns/sortable.rb11
-rw-r--r--app/models/concerns/taskable.rb16
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/group_label.rb4
-rw-r--r--app/models/issue.rb5
-rw-r--r--app/models/label.rb30
-rw-r--r--app/models/lfs_object.rb6
-rw-r--r--app/models/merge_request.rb1
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/project_label.rb4
-rw-r--r--app/models/project_services/bugzilla_service.rb2
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/gitlab_issue_tracker_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb18
-rw-r--r--app/models/project_services/jira_service.rb232
-rw-r--r--app/models/project_services/redmine_service.rb2
-rw-r--r--app/models/project_team.rb10
-rw-r--r--app/models/repository.rb38
-rw-r--r--app/models/todo.rb2
-rw-r--r--app/models/user.rb22
-rw-r--r--app/services/ci/process_pipeline_service.rb23
-rw-r--r--app/services/ci/register_build_service.rb11
-rw-r--r--app/services/delete_branch_service.rb2
-rw-r--r--app/services/delete_tag_service.rb2
-rw-r--r--app/services/git_tag_push_service.rb4
-rw-r--r--app/services/labels/find_or_create_service.rb11
-rw-r--r--app/services/members/approve_access_request_service.rb21
-rw-r--r--app/services/members/create_service.rb16
-rw-r--r--app/services/merge_requests/build_service.rb50
-rw-r--r--app/services/projects/import_service.rb2
-rw-r--r--app/views/admin/groups/_group.html.haml3
-rw-r--r--app/views/admin/groups/show.html.haml3
-rw-r--r--app/views/admin/projects/index.html.haml3
-rw-r--r--app/views/admin/users/index.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml147
-rw-r--r--app/views/groups/edit.html.haml3
-rw-r--r--app/views/groups/labels/index.html.haml2
-rw-r--r--app/views/groups/show.html.haml3
-rw-r--r--app/views/help/ui.html.haml2
-rw-r--r--app/views/kaminari/gitlab/_gap.html.haml4
-rw-r--r--app/views/kaminari/gitlab/_page.html.haml2
-rw-r--r--app/views/layouts/header/_default.html.haml6
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml85
-rw-r--r--app/views/projects/_home_panel.html.haml3
-rw-r--r--app/views/projects/boards/components/_board.html.haml4
-rw-r--r--app/views/projects/boards/components/_card.html.haml17
-rw-r--r--app/views/projects/boards/components/_sidebar.html.haml23
-rw-r--r--app/views/projects/boards/components/sidebar/_assignee.html.haml40
-rw-r--r--app/views/projects/boards/components/sidebar/_due_date.html.haml32
-rw-r--r--app/views/projects/boards/components/sidebar/_labels.html.haml30
-rw-r--r--app/views/projects/boards/components/sidebar/_milestone.html.haml28
-rw-r--r--app/views/projects/boards/components/sidebar/_notifications.html.haml11
-rw-r--r--app/views/projects/boards/index.html.haml10
-rw-r--r--app/views/projects/boards/show.html.haml10
-rw-r--r--app/views/projects/branches/_branch.html.haml9
-rw-r--r--app/views/projects/builds/_header.html.haml29
-rw-r--r--app/views/projects/builds/_sidebar.html.haml2
-rw-r--r--app/views/projects/buttons/_fork.html.haml4
-rw-r--r--app/views/projects/buttons/_star.html.haml2
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml7
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml7
-rw-r--r--app/views/projects/commit/_commit_box.html.haml74
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml2
-rw-r--r--app/views/projects/commit/_pipelines_list.haml2
-rw-r--r--app/views/projects/commit/builds.html.haml3
-rw-r--r--app/views/projects/commit/pipelines.html.haml3
-rw-r--r--app/views/projects/commit/show.html.haml3
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml4
-rw-r--r--app/views/projects/diffs/_file.html.haml1
-rw-r--r--app/views/projects/diffs/_file_header.html.haml2
-rw-r--r--app/views/projects/diffs/_parallel_view.html.haml2
-rw-r--r--app/views/projects/edit.html.haml3
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml4
-rw-r--r--app/views/projects/issues/_related_branches.html.haml2
-rw-r--r--app/views/projects/labels/index.html.haml4
-rw-r--r--app/views/projects/pipelines_settings/show.html.haml57
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/shared/_event_filter.html.haml9
-rw-r--r--app/views/shared/_label.html.haml13
-rw-r--r--app/views/shared/empty_states/_todos_all_done.svg1
-rw-r--r--app/views/shared/empty_states/_todos_empty.svg110
-rw-r--r--app/views/shared/groups/_group.html.haml3
-rw-r--r--app/views/shared/icons/_icon_close.svg1
-rw-r--r--app/views/shared/milestones/_issuable.html.haml3
-rw-r--r--app/views/shared/projects/_project.html.haml9
-rw-r--r--app/views/users/_groups.html.haml3
-rw-r--r--app/workers/project_cache_worker.rb16
-rw-r--r--app/workers/project_web_hook_worker.rb2
-rw-r--r--app/workers/remove_unreferenced_lfs_objects_worker.rb8
-rw-r--r--config/application.rb1
-rw-r--r--config/dependency_decisions.yml7
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--config/initializers/0_post_deployment_migrations.rb12
-rw-r--r--config/initializers/1_settings.rb3
-rw-r--r--config/initializers/sidekiq.rb16
-rw-r--r--config/mail_room.yml17
-rw-r--r--config/routes/group.rb33
-rw-r--r--db/migrate/20161011222551_remove_inactive_jira_service_properties.rb10
-rw-r--r--db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb14
-rw-r--r--db/migrate/20161025231710_migrate_jira_to_gem.rb73
-rw-r--r--db/post_migrate/.gitkeep0
-rw-r--r--db/schema.rb4
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/deploy_keys.md2
-rw-r--r--doc/api/projects.md6
-rw-r--r--doc/api/services.md46
-rw-r--r--doc/api/tags.md2
-rw-r--r--doc/ci/README.md1
-rw-r--r--doc/ci/docker/using_docker_build.md8
-rw-r--r--doc/ci/pipelines.md49
-rw-r--r--doc/development/README.md3
-rw-r--r--doc/development/api_styleguide.md96
-rw-r--r--doc/development/doc_styleguide.md10
-rw-r--r--doc/development/frontend.md25
-rw-r--r--doc/development/gotchas.md6
-rw-r--r--doc/development/licensing.md3
-rw-r--r--doc/development/post_deployment_migrations.md75
-rw-r--r--doc/development/testing.md4
-rw-r--r--doc/install/installation.md3
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/integration/img/jira_add_user_to_group.png (renamed from doc/project_services/img/jira_add_user_to_group.png)bin41994 -> 41994 bytes
-rw-r--r--doc/integration/img/jira_create_new_group.png (renamed from doc/project_services/img/jira_create_new_group.png)bin32934 -> 32934 bytes
-rw-r--r--doc/integration/img/jira_create_new_group_name.png (renamed from doc/project_services/img/jira_create_new_group_name.png)bin9054 -> 9054 bytes
-rw-r--r--doc/integration/img/jira_create_new_user.png (renamed from doc/project_services/img/jira_create_new_user.png)bin21081 -> 21081 bytes
-rw-r--r--doc/integration/img/jira_group_access.png (renamed from doc/project_services/img/jira_group_access.png)bin32210 -> 32210 bytes
-rw-r--r--doc/integration/img/jira_issue_reference.png (renamed from doc/project_services/img/jira_issue_reference.png)bin36188 -> 36188 bytes
-rw-r--r--doc/integration/img/jira_merge_request_close.pngbin0 -> 52556 bytes
-rw-r--r--doc/integration/img/jira_project_name.png (renamed from doc/project_services/img/jira_project_name.png)bin41572 -> 41572 bytes
-rw-r--r--doc/integration/img/jira_service.png (renamed from doc/project_services/img/jira_service.png)bin56834 -> 56834 bytes
-rw-r--r--doc/integration/img/jira_service_close_issue.png (renamed from doc/project_services/img/jira_service_close_issue.png)bin79569 -> 79569 bytes
-rw-r--r--doc/integration/img/jira_service_page.pngbin0 -> 45089 bytes
-rw-r--r--doc/integration/img/jira_user_management_link.png (renamed from doc/project_services/img/jira_user_management_link.png)bin43095 -> 43095 bytes
-rw-r--r--doc/integration/img/jira_workflow_screenshot.png (renamed from doc/project_services/img/jira_workflow_screenshot.png)bin111093 -> 111093 bytes
-rw-r--r--doc/integration/jira.md196
-rw-r--r--doc/integration/saml.md21
-rw-r--r--doc/profile/two_factor_authentication.md16
-rw-r--r--doc/project_services/img/jira_add_gitlab_commit_message.pngbin46590 -> 0 bytes
-rw-r--r--doc/project_services/img/jira_issue_closed.pngbin77028 -> 0 bytes
-rw-r--r--doc/project_services/img/jira_issues_workflow.pngbin87067 -> 0 bytes
-rw-r--r--doc/project_services/img/jira_merge_request_close.pngbin102835 -> 0 bytes
-rw-r--r--doc/project_services/img/jira_reference_commit_message_in_jira_issue.pngbin33706 -> 0 bytes
-rw-r--r--doc/project_services/img/jira_service_page.pngbin36280 -> 0 bytes
-rw-r--r--doc/project_services/img/jira_submit_gitlab_merge_request.pngbin51913 -> 0 bytes
-rw-r--r--doc/project_services/jira.md247
-rw-r--r--doc/project_services/project_services.md2
-rw-r--r--doc/raketasks/backup_restore.md7
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/user/project/img/project_settings_list.pngbin10788 -> 11404 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_settings_badges.pngbin0 -> 56166 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_settings_test_coverage.pngbin0 -> 4212 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_test_coverage_build.pngbin0 -> 9953 bytes
-rw-r--r--doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.pngbin0 -> 14502 bytes
-rw-r--r--doc/user/project/pipelines/settings.md113
-rw-r--r--features/dashboard/active_tab.feature6
-rw-r--r--features/dashboard/dashboard.feature1
-rw-r--r--features/steps/project/commits/commits.rb4
-rw-r--r--features/steps/project/services.rb8
-rw-r--r--features/steps/shared/sidebar_active_tab.rb14
-rw-r--r--generator_templates/rails/post_deployment_migration/migration.rb22
-rw-r--r--lib/api/branches.rb123
-rw-r--r--lib/api/deploy_keys.rb9
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/helpers.rb9
-rw-r--r--lib/api/internal.rb13
-rw-r--r--lib/api/tags.rb94
-rw-r--r--lib/api/users.rb4
-rw-r--r--lib/backup/repository.rb92
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb20
-rw-r--r--lib/banzai/filter/external_issue_reference_filter.rb11
-rw-r--r--lib/banzai/filter/label_reference_filter.rb2
-rw-r--r--lib/banzai/filter/reference_filter.rb6
-rw-r--r--lib/banzai/filter/user_reference_filter.rb41
-rw-r--r--lib/banzai/redactor.rb8
-rw-r--r--lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb15
-rw-r--r--lib/gitlab/data_builder/push.rb2
-rw-r--r--lib/gitlab/exclusive_lease.rb9
-rw-r--r--lib/gitlab/fogbugz_import/importer.rb4
-rw-r--r--lib/gitlab/github_import/base_formatter.rb4
-rw-r--r--lib/gitlab/github_import/client.rb12
-rw-r--r--lib/gitlab/github_import/importer.rb115
-rw-r--r--lib/gitlab/github_import/issue_formatter.rb8
-rw-r--r--lib/gitlab/github_import/label_formatter.rb8
-rw-r--r--lib/gitlab/github_import/milestone_formatter.rb8
-rw-r--r--lib/gitlab/github_import/pull_request_formatter.rb8
-rw-r--r--lib/gitlab/github_import/release_formatter.rb8
-rw-r--r--lib/gitlab/google_code_import/importer.rb4
-rw-r--r--lib/gitlab/issues_labels.rb2
-rw-r--r--lib/gitlab/mail_room.rb7
-rw-r--r--lib/gitlab/optimistic_locking.rb19
-rw-r--r--lib/gitlab/redis.rb8
-rw-r--r--lib/gitlab/utils.rb8
-rw-r--r--lib/tasks/eslint.rake7
-rw-r--r--lib/tasks/lint.rake9
-rw-r--r--lib/tasks/teaspoon.rake25
-rw-r--r--package.json14
-rw-r--r--spec/config/mail_room_spec.rb26
-rw-r--r--spec/controllers/admin/users_controller_spec.rb11
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb4
-rw-r--r--spec/controllers/projects/labels_controller_spec.rb21
-rw-r--r--spec/controllers/projects/milestones_controller_spec.rb2
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb68
-rw-r--r--spec/controllers/snippets_controller_spec.rb156
-rw-r--r--spec/factories/projects.rb7
-rw-r--r--spec/features/boards/boards_spec.rb21
-rw-r--r--spec/features/boards/new_issue_spec.rb15
-rw-r--r--spec/features/boards/sidebar_spec.rb312
-rw-r--r--spec/features/issues/filter_by_milestone_spec.rb16
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb8
-rw-r--r--spec/features/merge_requests/created_from_fork_spec.rb3
-rw-r--r--spec/features/merge_requests/diff_notes_resolve_spec.rb10
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb17
-rw-r--r--spec/features/milestones/milestones_spec.rb86
-rw-r--r--spec/features/projects/branches/delete_spec.rb24
-rw-r--r--spec/features/projects/builds_spec.rb4
-rw-r--r--spec/features/projects/features_visibility_spec.rb34
-rw-r--r--spec/features/projects/issuable_templates_spec.rb2
-rw-r--r--spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb44
-rw-r--r--spec/features/todos/todos_sorting_spec.rb126
-rw-r--r--spec/features/todos/todos_spec.rb9
-rw-r--r--spec/finders/branches_finder_spec.rb2
-rw-r--r--spec/finders/tags_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/issue.json4
-rw-r--r--spec/javascripts/abuse_reports_spec.js.es61
-rw-r--r--spec/javascripts/activities_spec.js.es63
-rw-r--r--spec/javascripts/application_spec.js1
-rw-r--r--spec/javascripts/awards_handler_spec.js26
-rw-r--r--spec/javascripts/behaviors/autosize_spec.js1
-rw-r--r--spec/javascripts/behaviors/quick_submit_spec.js1
-rw-r--r--spec/javascripts/behaviors/requires_input_spec.js1
-rw-r--r--spec/javascripts/boards/boards_store_spec.js.es68
-rw-r--r--spec/javascripts/boards/issue_spec.js.es63
-rw-r--r--spec/javascripts/boards/list_spec.js.es63
-rw-r--r--spec/javascripts/boards/mock_data.js.es61
-rw-r--r--spec/javascripts/dashboard_spec.js.es639
-rw-r--r--spec/javascripts/datetime_utility_spec.js.es61
-rw-r--r--spec/javascripts/diff_comments_store_spec.js.es61
-rw-r--r--spec/javascripts/extensions/array_spec.js1
-rw-r--r--spec/javascripts/extensions/jquery_spec.js1
-rw-r--r--spec/javascripts/fixtures/.gitignore1
-rw-r--r--spec/javascripts/fixtures/dashboard.html.haml45
-rw-r--r--spec/javascripts/fixtures/emoji_menu.js1
-rw-r--r--spec/javascripts/fixtures/header.html.haml35
-rw-r--r--spec/javascripts/fixtures/issues.rb44
-rw-r--r--spec/javascripts/fixtures/issues_show.html.haml23
-rw-r--r--spec/javascripts/fixtures/right_sidebar.html.haml4
-rw-r--r--spec/javascripts/fixtures/todos.json4
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es679
-rw-r--r--spec/javascripts/gl_field_errors_spec.js.es61
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js1
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_util_spec.js1
-rw-r--r--spec/javascripts/graphs/stat_graph_spec.js1
-rw-r--r--spec/javascripts/header_spec.js55
-rw-r--r--spec/javascripts/issue_spec.js185
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es61
-rw-r--r--spec/javascripts/line_highlighter_spec.js1
-rw-r--r--spec/javascripts/merge_request_spec.js1
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js1
-rw-r--r--spec/javascripts/merge_request_widget_spec.js1
-rw-r--r--spec/javascripts/new_branch_spec.js1
-rw-r--r--spec/javascripts/notes_spec.js1
-rw-r--r--spec/javascripts/project_title_spec.js1
-rw-r--r--spec/javascripts/right_sidebar_spec.js22
-rw-r--r--spec/javascripts/search_autocomplete_spec.js1
-rw-r--r--spec/javascripts/shortcuts_issuable_spec.js1
-rw-r--r--spec/javascripts/spec_helper.js3
-rw-r--r--spec/javascripts/syntax_highlight_spec.js1
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js1
-rw-r--r--spec/javascripts/u2f/mock_u2f_device.js1
-rw-r--r--spec/javascripts/u2f/register_spec.js1
-rw-r--r--spec/javascripts/zen_mode_spec.js1
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb38
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/pipeline/full_pipeline_spec.rb28
-rw-r--r--spec/lib/banzai/redactor_spec.rb75
-rw-r--r--spec/lib/gitlab/exclusive_lease_spec.rb43
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb8
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml2
-rw-r--r--spec/lib/gitlab/optimistic_locking_spec.rb39
-rw-r--r--spec/lib/gitlab/redis_spec.rb49
-rw-r--r--spec/lib/gitlab/utils_spec.rb35
-rw-r--r--spec/models/ci/pipeline_spec.rb28
-rw-r--r--spec/models/concerns/issuable_spec.rb14
-rw-r--r--spec/models/concerns/project_features_compatibility_spec.rb14
-rw-r--r--spec/models/group_spec.rb6
-rw-r--r--spec/models/members/project_member_spec.rb4
-rw-r--r--spec/models/project_services/jira_service_spec.rb71
-rw-r--r--spec/models/project_spec.rb6
-rw-r--r--spec/models/repository_spec.rb24
-rw-r--r--spec/models/user_spec.rb74
-rw-r--r--spec/policies/issues_policy_spec.rb193
-rw-r--r--spec/requests/api/api_helpers_spec.rb23
-rw-r--r--spec/requests/api/branches_spec.rb12
-rw-r--r--spec/requests/api/builds_spec.rb2
-rw-r--r--spec/requests/api/deploy_keys_spec.rb17
-rw-r--r--spec/requests/api/users_spec.rb23
-rw-r--r--spec/services/ci/register_build_service_spec.rb4
-rw-r--r--spec/services/git_push_service_spec.rb23
-rw-r--r--spec/services/git_tag_push_service_spec.rb173
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb51
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb63
-rw-r--r--spec/services/members/create_service_spec.rb25
-rw-r--r--spec/services/merge_requests/build_service_spec.rb49
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb2
-rw-r--r--spec/services/milestones/close_service_spec.rb2
-rw-r--r--spec/services/projects/create_service_spec.rb2
-rw-r--r--spec/services/projects/import_service_spec.rb2
-rw-r--r--spec/services/system_note_service_spec.rb58
-rw-r--r--spec/spec_helper.rb8
-rw-r--r--spec/support/banzai/reference_filter_shared_examples.rb13
-rw-r--r--spec/support/javascript_fixtures_helpers.rb45
-rw-r--r--spec/support/jira_service_helper.rb42
-rw-r--r--spec/support/taskable_shared_examples.rb6
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb74
-rw-r--r--spec/views/projects/commit/_commit_box.html.haml_spec.rb28
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb2
-rw-r--r--spec/workers/project_cache_worker_spec.rb20
-rw-r--r--spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb55
-rw-r--r--vendor/assets/javascripts/jquery.cookie.js41
-rw-r--r--vendor/assets/javascripts/js.cookie.js156
577 files changed, 6592 insertions, 2293 deletions
diff --git a/.eslintignore b/.eslintignore
new file mode 100644
index 00000000000..453747e14e1
--- /dev/null
+++ b/.eslintignore
@@ -0,0 +1,4 @@
+/public/
+/tmp/
+/vendor/
+
diff --git a/.eslintrc b/.eslintrc
new file mode 100644
index 00000000000..16eb18ecba2
--- /dev/null
+++ b/.eslintrc
@@ -0,0 +1,23 @@
+{
+ "extends": "airbnb",
+ "globals": {
+ "$": false,
+ "_": false,
+ "beforeEach": false,
+ "d3": false,
+ "define": false,
+ "describe": false,
+ "document": false,
+ "expect": false,
+ "fixture": false,
+ "gl": false,
+ "it": false,
+ "jQuery": false,
+ "Mousetrap": false,
+ "spyOn": false,
+ "spyOnEvent": false,
+ "Turbolinks": false,
+ "window": false
+ }
+}
+
diff --git a/.gitignore b/.gitignore
index 9166512606d..6a1002621f4 100644
--- a/.gitignore
+++ b/.gitignore
@@ -37,6 +37,7 @@
/doc/code/*
/dump.rdb
/log/*.log*
+/node_modules/
/nohup.out
/public/assets/
/public/uploads.*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 9c4b4acbaf5..d04069df885 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -215,6 +215,7 @@ rake ee_compat_check:
only:
- branches
except:
+ - master
- tags
allow_failure: yes
@@ -248,7 +249,7 @@ teaspoon:
- curl --silent --location https://deb.nodesource.com/setup_6.x | bash -
- apt-get install --assume-yes nodejs
- npm install --global istanbul
- - teaspoon
+ - rake teaspoon
artifacts:
name: coverage-javascript
expire_in: 31d
@@ -279,16 +280,20 @@ bundler:audit:
migration paths:
stage: test
<<: *use-db
+ variables:
+ SETUP_DB: "false"
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'
+ - cp config/resque.yml.example config/resque.yml
+ - sed -i 's/localhost/redis/g' 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
+ - source scripts/prepare_build.sh
- rake db:migrate
coverage:
@@ -306,16 +311,29 @@ coverage:
- coverage/index.html
- coverage/assets/
+lint-javascript:
+ stage: test
+ image: "node:latest"
+ before_script:
+ - npm install
+ script:
+ - npm run eslint
+
# Trigger docs build
+# https://gitlab.com/gitlab-com/doc-gitlab-com/blob/master/README.md#deployment-process
trigger_docs:
stage: post-test
- before_script: []
+ image: "alpine"
+ before_script:
+ - apk update && apk add curl
+ variables:
+ GIT_STRATEGY: none
cache: {}
artifacts: {}
script:
- - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master https://gitlab.com/api/v3/projects/38069/trigger/builds"
+ - "curl -X POST -F token=${DOCS_TRIGGER_TOKEN} -F ref=master -F variables[PROJECT]=ce https://gitlab.com/api/v3/projects/38069/trigger/builds"
only:
- - master
+ - master@gitlab-org/gitlab-ce
# Notify slack in the end
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 5c8e5ac0758..aae8d9b6dbe 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -143,7 +143,7 @@ linters:
# with two colons. Pseudo-classes, like :hover and :first-child, should
# be declared with one colon.
PseudoElement:
- enabled: false
+ enabled: true
# Avoid qualifying elements in selectors (also known as "tag-qualifying").
QualifyingElement:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a034ad416b7..bd62b6839a8 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -29,148 +29,249 @@ Please view this file on the master branch, on stable branches it's out of date.
- Better handle when no users were selected for adding to group or project. (Linus Thiel)
- Only show register tab if signup enabled.
+- Backups do not fail anymore when using tar on annex and custom_hooks only. !5814
+- Adds user project membership expired event to clarify why user was removed (Callum Dryden)
+- Trim leading and trailing whitespace on project_path (Linus Thiel)
+- Prevent award emoji via notes for issues/MRs authored by user (barthc)
+- Adds an optional path parameter to the Commits API to filter commits by path (Luis HGO)
+- Fix Markdown styling inside reference links (Jan Zdráhal)
+- Fix extra space on Build sidebar on Firefox !7060
+- Fail gracefully when creating merge request with non-existing branch (alexsanford)
+- Fix mobile layout issues in admin user overview page !7087
+- Fix HipChat notifications rendering (airatshigapov, eisnerd)
+- Refactor Jira service to use jira-ruby gem
+- Improved todos empty state
+- Add hover to trash icon in notes !7008 (blackst0ne)
+- Hides project activity tabs when features are disabled
+- Only show one error message for an invalid email !5905 (lycoperdon)
+- Fix sidekiq stats in admin area (blackst0ne)
+- Added label description as tooltip to issue board list title
+- Created cycle analytics bundle JavaScript file
+- API: Fix booleans not recognized as such when using the `to_boolean` helper
+- Removed delete branch tooltip !6954
+- Stop unauthorized users dragging on milestone page (blackst0ne)
+- Restore issue boards welcome message when a project is created !6899
+- Do not show tooltip for active element !7105 (winniehell)
+- Escape ref and path for relative links !6050 (winniehell)
+- Fixed link typo on /help/ui to Alerts section. !6915 (Sam Rose)
+- Fix filtering of milestones with quotes in title (airatshigapov)
+- Refactor less readable existance checking code from CoffeeScript !6289 (jlogandavison)
+- Update mail_room and enable sentinel support to Reply By Email (!7101)
+- Add task completion status in Issues and Merge Requests tabs: "X of Y tasks completed" (!6527, @gmesalazar)
+- Simpler arguments passed to named_route on toggle_award_url helper method
+- Fix typo in framework css class. !7086 (Daniel Voogsgerd)
+- New issue board list dropdown stays open after adding a new list
+- Fix: Backup restore doesn't clear cache
+- Optimize Event queries by removing default order
+- Remove duplicate links from sidebar
+- API: Fix project deploy keys 400 and 500 errors when adding an existing key. !6784 (Joshua Welsh)
+- Add job for removal of unreferenced LFS objects from both the database and the filesystem (Frank Groeneveld)
+- Replace jquery.cookie plugin with js.cookie !7085
+- Use MergeRequestsClosingIssues cache data on Issue#closed_by_merge_requests method
+- Fix Sign in page 'Forgot your password?' link overlaps on medium-large screens
+- Show full status link on MR & commit pipelines
+- Fix documents and comments on Build API `scope`
+- Initialize Sidekiq with the list of queues used by GitLab
+- Refactor email, use setter method instead AR callbacks for email attribute (Semyon Pupkov)
+- Shortened merge request modal to let clipboard button not overlap
+- In all filterable drop downs, put input field in focus only after load is complete (Ido @leibo)
+- Improve search query parameter naming in /admin/users !7115 (YarNayar)
+- Fix table pagination to be responsive
+- Allow to search for user by secondary email address in the admin interface(/admin/users) !7115 (YarNayar)
+
+## 8.13.3
+
+- Fix relative links in Markdown wiki when displayed in "Project" tab !7218
+- Reduce the overhead to calculate number of open/closed issues and merge requests within the group or project
+- Fix project features default values
+
+## 8.13.2 (2016-10-31)
+
+- Fix encoding issues on pipeline commits. !6832
+- Use Hash rocket syntax to fix cycle analytics under Ruby 2.1. !6977
+- Modify GitHub importer to be retryable. !7003
+- Fix refs dropdown selection with special characters. !7061
+- Fix horizontal padding for highlight blocks. !7062
+- Pass user instance to `Labels::FindOrCreateService` or `skip_authorization: true`. !7093
+- Fix builds dropdown overlapping bug. !7124
+- Fix applying labels for GitHub-imported MRs. !7139
+- Fix importing MR comments from GitHub. !7139
+- Fix project member access for group links. !7144
+- API: Fix booleans not recognized as such when using the `to_boolean` helper. !7149
+- Fix and improve `Sortable.highest_label_priority`. !7165
+- Fixed sticky merge request tabs when sidebar is pinned. !7167
+- Only remove right connector of first build of last stage. !7179
+
+## 8.13.1 (2016-10-25)
+
+- Fix branch protection API. !6215
+- Fix hidden pipeline graph on commit and MR page. !6895
+- Fix Cycle analytics not showing correct data when filtering by date. !6906
+- Ensure custom provider tab labels don't break layout. !6993
+- Fix issue boards user link when in subdirectory. !7018
+- Refactor and add new environment functionality to CI yaml reference. !7026
+- Fix typo in project settings that prevents users from enabling container registry. !7037
+- Fix events order in `users/:id/events` endpoint. !7039
+- Remove extra line for empty issue description. !7045
+- Don't append issue/MR templates to any existing text. !7050
+- Fix error in generating labels. !7055
+- Stop clearing the database cache on `rake cache:clear`. !7056
+- Only show register tab if signup enabled. !7058
+- Fix lightweight tags not processed correctly by GitTagPushService
+- Expire and build repository cache after project import. !7064
+- Fix bug where labels would be assigned to issues that were moved. !7065
+- Fix reply-by-email not working due to queue name mismatch. !7068
+- Fix 404 for group pages when GitLab setup uses relative url. !7071
+- Fix `User#to_reference`. !7088
+- Reduce overhead of `LabelFinder` by avoiding `#presence` call. !7094
+- Fix unauthorized users dragging on issue boards. !7096
+- Only schedule `ProjectCacheWorker` jobs when needed. !7099
+
## 8.13.0 (2016-10-22)
- - Removes extra line for empty issue description. (!7045)
- - Fix save button on project pipeline settings page. (!6955)
- - All Sidekiq workers now use their own queue
- - Avoid race condition when asynchronously removing expired artifacts. (!6881)
- - Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
- - Respond with 404 Not Found for non-existent tags (Linus Thiel)
- - Truncate long labels with ellipsis in labels page
- - Improve tabbing usability for sign in page (ClemMakesApps)
- - Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint
- - Adding members no longer silently fails when there is extra whitespace
- - Update runner version only when updating contacted_at
- - Add link from system note to compare with previous version
- - Use gitlab-shell v3.6.6
- - Ignore references to internal issues when using external issues tracker
- - Ability to resolve merge request conflicts with editor !6374
- - Add `/projects/visible` API endpoint (Ben Boeckel)
- - Fix centering of custom header logos (Ashley Dumaine)
- - Keep around commits only pipeline creation as pipeline data doesn't change over time
- - Update duration at the end of pipeline
- - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
- - Add group level labels. (!6425)
- - Fix Cycle analytics not showing correct data when filtering by date. !6906
- - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
- - Cancelled pipelines could be retried. !6927
- - Updating verbiage on git basics to be more intuitive
- - Fix project_feature record not generated on project creation
- - Clarify documentation for Runners API (Gennady Trafimenkov)
- - The instrumentation for Banzai::Renderer has been restored
- - Change user & group landing page routing from /u/:username to /:username
- - Fixed issue boards user link when in subdirectory
- - Added documentation for .gitattributes files
- - Move Pipeline Metrics to separate worker
- - AbstractReferenceFilter caches project_refs on RequestStore when active
- - Replaced the check sign to arrow in the show build view. !6501
- - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
- - ProjectCacheWorker updates caches at most once per 15 minutes per project
- - Fix Error 500 when viewing old merge requests with bad diff data
- - Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar)
- - Fix viewing merged MRs when the source project has been removed !6991
- - Speed-up group milestones show page
- - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
- - Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService
- - Fix discussion thread from emails for merge requests. !7010
- - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
- - Add tag shortcut from the Commit page. !6543
- - Keep refs for each deployment
- - Allow browsing branches that end with '.atom'
- - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
- - Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
- - Add more tests for calendar contribution (ClemMakesApps)
- - Update Gitlab Shell to fix some problems with moving projects between storages
- - Cache rendered markdown in the database, rather than Redis
- - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
- - Simplify Mentionable concern instance methods
- - API: Ability to retrieve version information (Robert Schilling)
- - Fix permission for setting an issue's due date
- - API: Multi-file commit !6096 (mahcsig)
- - Unicode emoji are now converted to images
- - Revert "Label list shows all issues (opened or closed) with that label"
- - Expose expires_at field when sharing project on API
- - Fix VueJS template tags being rendered in code comments
- - Added copy file path button to merge request diff files
- - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
- - Add Issue Board API support (andrebsguedes)
- - Allow the Koding integration to be configured through the API
- - Add new issue button to each list on Issues Board
- - Execute specific named route method from toggle_award_url helper method
- - Added soft wrap button to repository file/blob editor
- - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
- - Show the time ago a merge request was deployed to an environment
- - Add RTL support to markdown renderer (Ebrahim Byagowi)
- - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
- - Fix todos page mobile viewport layout (ClemMakesApps)
- - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
- - Remove redundant mixins (ClemMakesApps)
- - Added 'Download' button to the Snippets page (Justin DiPierro)
- - Add visibility level to project repository
- - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
- - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
- - Fix showing commits from source project for merge request !6658
- - Fix that manual jobs would no longer block jobs in the next stage. !6604
- - Add configurable email subject suffix (Fu Xu)
- - Use defined colour for a language when available !6748 (nilsding)
- - Added tooltip to fork count on project show page. (Justin DiPierro)
- - Use a ConnectionPool for Rails.cache on Sidekiq servers
- - Replace `alias_method_chain` with `Module#prepend`
- - Enable GitLab Import/Export for non-admin users.
- - Preserve label filters when sorting !6136 (Joseph Frazier)
- - MergeRequest#new form load diff asynchronously
- - Only update issuable labels if they have been changed
- - Take filters in account in issuable counters. !6496
- - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
- - Append issue template to existing description !6149 (Joseph Frazier)
- - Trending projects now only show public projects and the list of projects is cached for a day
- - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
- - Revoke button in Applications Settings underlines on hover.
- - Use higher size on Gitlab::Redis connection pool on Sidekiq servers
- - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
- - Revert avoid touching file system on Build#artifacts?
- - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
- - Add disabled delete button to protected branches (ClemMakesApps)
- - Add broadcast messages and alerts below sub-nav
- - Better empty state for Groups view
- - API: New /users/:id/events endpoint
- - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
- - Replace bootstrap caret with fontawesome caret (ClemMakesApps)
- - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
- - Add organization field to user profile
- - Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
- - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
- - Fix deploy status responsiveness error !6633
- - Make searching for commits case insensitive
- - Fix resolved discussion display in side-by-side diff view !6575
- - Optimize GitHub importing for speed and memory
- - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
- - Notify the Merger about merge after successful build (Dimitris Karakasilis)
- - Reduce queries needed to find users using their SSH keys when pushing commits
- - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
- - Fix broken repository 500 errors in project list
- - Fix the diff in the merge request view when converting a symlink to a regular file
- - Fix Pipeline list commit column width should be adjusted
- - Close todos when accepting merge requests via the API !6486 (tonygambone)
- - Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
- - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
- - Retouch environments list and deployments list
- - Add multiple command support for all label related slash commands !6780 (barthc)
- - Add Container Registry on/off status to Admin Area !6638 (the-undefined)
- - Add Nofollow for uppercased scheme in external urls !6820 (the-undefined)
- - Allow empty merge requests !6384 (Artem Sidorenko)
- - Grouped pipeline dropdown is a scrollable container
- - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
- - Fixes padding in all clipboard icons that have .btn class
- - Fix a typo in doc/api/labels.md
- - Fix double-escaping in activities tab (Alexandre Maia)
- - API: all unknown routing will be handled with 404 Not Found
- - Add docs for request profiling
- - Delete dynamic environments
- - Fix buggy iOS tooltip layering behavior.
- - Make guests unable to view MRs on private projects
- - Fix broken Project API docs (Takuya Noguchi)
- - Migrate invalid project members (owner -> master)
+
+- Fix save button on project pipeline settings page. (!6955)
+- All Sidekiq workers now use their own queue
+- Avoid race condition when asynchronously removing expired artifacts. (!6881)
+- Improve Merge When Build Succeeds triggers and execute on pipeline success. (!6675)
+- Respond with 404 Not Found for non-existent tags (Linus Thiel)
+- Truncate long labels with ellipsis in labels page
+- Improve tabbing usability for sign in page (ClemMakesApps)
+- Enforce TrailingSemicolon and EmptyLineBetweenBlocks in scss-lint
+- Adding members no longer silently fails when there is extra whitespace
+- Update runner version only when updating contacted_at
+- Add link from system note to compare with previous version
+- Use gitlab-shell v3.6.6
+- Ignore references to internal issues when using external issues tracker
+- Ability to resolve merge request conflicts with editor !6374
+- Add `/projects/visible` API endpoint (Ben Boeckel)
+- Fix centering of custom header logos (Ashley Dumaine)
+- Keep around commits only pipeline creation as pipeline data doesn't change over time
+- Update duration at the end of pipeline
+- ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
+- Add group level labels. (!6425)
+- Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
+- Cancelled pipelines could be retried. !6927
+- Updating verbiage on git basics to be more intuitive
+- Fix project_feature record not generated on project creation
+- Clarify documentation for Runners API (Gennady Trafimenkov)
+- Use optimistic locking for pipelines and builds
+- The instrumentation for Banzai::Renderer has been restored
+- Change user & group landing page routing from /u/:username to /:username
+- Added documentation for .gitattributes files
+- Move Pipeline Metrics to separate worker
+- AbstractReferenceFilter caches project_refs on RequestStore when active
+- Replaced the check sign to arrow in the show build view. !6501
+- Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
+- ProjectCacheWorker updates caches at most once per 15 minutes per project
+- Fix Error 500 when viewing old merge requests with bad diff data
+- Create a new /templates namespace for the /licenses, /gitignores and /gitlab_ci_ymls API endpoints. !5717 (tbalthazar)
+- Fix viewing merged MRs when the source project has been removed !6991
+- Speed-up group milestones show page
+- Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
+- Extract project#update_merge_requests and SystemHooks to its own worker from GitPushService
+- Fix discussion thread from emails for merge requests. !7010
+- Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
+- Add tag shortcut from the Commit page. !6543
+- Keep refs for each deployment
+- Close open tooltips on page navigation (Linus Thiel)
+- Allow browsing branches that end with '.atom'
+- Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
+- Replace unique keyframes mixin with keyframe mixin with specific names (ClemMakesApps)
+- Add more tests for calendar contribution (ClemMakesApps)
+- Update Gitlab Shell to fix some problems with moving projects between storages
+- Cache rendered markdown in the database, rather than Redis
+- Add todo toggle event (ClemMakesApps)
+- Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
+- Simplify Mentionable concern instance methods
+- API: Ability to retrieve version information (Robert Schilling)
+- Fix permission for setting an issue's due date
+- API: Multi-file commit !6096 (mahcsig)
+- Unicode emoji are now converted to images
+- Revert "Label list shows all issues (opened or closed) with that label"
+- Expose expires_at field when sharing project on API
+- Fix VueJS template tags being rendered in code comments
+- Added copy file path button to merge request diff files
+- Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
+- Add Issue Board API support (andrebsguedes)
+- Allow the Koding integration to be configured through the API
+- Add new issue button to each list on Issues Board
+- Execute specific named route method from toggle_award_url helper method
+- Added soft wrap button to repository file/blob editor
+- Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
+- Show the time ago a merge request was deployed to an environment
+- Add RTL support to markdown renderer (Ebrahim Byagowi)
+- Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
+- Fix todos page mobile viewport layout (ClemMakesApps)
+- Make issues search less finicky
+- Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
+- Remove redundant mixins (ClemMakesApps)
+- Added 'Download' button to the Snippets page (Justin DiPierro)
+- Add visibility level to project repository
+- Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
+- Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
+- Fix showing commits from source project for merge request !6658
+- Fix that manual jobs would no longer block jobs in the next stage. !6604
+- Add configurable email subject suffix (Fu Xu)
+- Use defined colour for a language when available !6748 (nilsding)
+- Added tooltip to fork count on project show page. (Justin DiPierro)
+- Use a ConnectionPool for Rails.cache on Sidekiq servers
+- Replace `alias_method_chain` with `Module#prepend`
+- Enable GitLab Import/Export for non-admin users.
+- Preserve label filters when sorting !6136 (Joseph Frazier)
+- MergeRequest#new form load diff asynchronously
+- Only update issuable labels if they have been changed
+- Take filters in account in issuable counters. !6496
+- Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
+- Replace static issue fixtures by script !6059 (winniehell)
+- Append issue template to existing description !6149 (Joseph Frazier)
+- Trending projects now only show public projects and the list of projects is cached for a day
+- Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
+- Revoke button in Applications Settings underlines on hover.
+- Use higher size on Gitlab::Redis connection pool on Sidekiq servers
+- Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
+- Revert avoid touching file system on Build#artifacts?
+- Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
+- Add disabled delete button to protected branches (ClemMakesApps)
+- Add broadcast messages and alerts below sub-nav
+- Better empty state for Groups view
+- API: New /users/:id/events endpoint
+- Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
+- Replace bootstrap caret with fontawesome caret (ClemMakesApps)
+- Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
+- Add organization field to user profile
+- Change user pages routing from /u/:username/PATH to /users/:username/PATH. Old routes will redirect to the new ones for the time being.
+- Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
+- Fix deploy status responsiveness error !6633
+- Make searching for commits case insensitive
+- Fix resolved discussion display in side-by-side diff view !6575
+- Optimize GitHub importing for speed and memory
+- API: expose pipeline data in builds API (!6502, Guilherme Salazar)
+- Notify the Merger about merge after successful build (Dimitris Karakasilis)
+- Reduce queries needed to find users using their SSH keys when pushing commits
+- Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
+- Fix broken repository 500 errors in project list
+- Fix the diff in the merge request view when converting a symlink to a regular file
+- Fix Pipeline list commit column width should be adjusted
+- Close todos when accepting merge requests via the API !6486 (tonygambone)
+- Ability to batch assign issues relating to a merge request to the author. !5725 (jamedjo)
+- Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
+- Retouch environments list and deployments list
+- Add multiple command support for all label related slash commands !6780 (barthc)
+- Add Container Registry on/off status to Admin Area !6638 (the-undefined)
+- Add Nofollow for uppercased scheme in external urls !6820 (the-undefined)
+- Allow empty merge requests !6384 (Artem Sidorenko)
+- Grouped pipeline dropdown is a scrollable container
+- Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
+- Fixes padding in all clipboard icons that have .btn class
+- Fix a typo in doc/api/labels.md
+- Fix double-escaping in activities tab (Alexandre Maia)
+- API: all unknown routing will be handled with 404 Not Found
+- Add docs for request profiling
+- Delete dynamic environments
+- Fix buggy iOS tooltip layering behavior.
+- Make guests unable to view MRs on private projects
+- Fix broken Project API docs (Takuya Noguchi)
+- Migrate invalid project members (owner -> master)
## 8.12.7
@@ -404,7 +505,6 @@ Please view this file on the master branch, on stable branches it's out of date.
- 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 branch protection API !6215
- 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
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 4f2c1d15f6d..fcdb2e109f6 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.6.6
+4.0.0
diff --git a/Gemfile b/Gemfile
index 46245ab62d1..7e94c4051da 100644
--- a/Gemfile
+++ b/Gemfile
@@ -51,7 +51,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.6.8'
+gem 'gitlab_git', '~> 10.7.0'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -161,6 +161,9 @@ gem 'connection_pool', '~> 2.0'
# HipChat integration
gem 'hipchat', '~> 1.5.0'
+# JIRA integration
+gem 'jira-ruby', '~> 1.1.2'
+
# Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
@@ -326,7 +329,7 @@ gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0'
-gem 'mail_room', '~> 0.8.1'
+gem 'mail_room', '~> 0.9.0'
gem 'email_reply_parser', '~> 0.5.8'
diff --git a/Gemfile.lock b/Gemfile.lock
index 442184b9228..81af1ad2dac 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -283,7 +283,7 @@ GEM
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab-markup (1.5.0)
- gitlab_git (10.6.8)
+ gitlab_git (10.7.0)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
@@ -356,6 +356,9 @@ GEM
cause
json
ipaddress (0.8.3)
+ jira-ruby (1.1.2)
+ activesupport
+ oauth (~> 0.5, >= 0.5.0)
jquery-atwho-rails (1.3.2)
jquery-rails (4.1.1)
rails-dom-testing (>= 1, < 3)
@@ -402,7 +405,7 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.8.1)
+ mail_room (0.9.0)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
@@ -421,7 +424,7 @@ GEM
mini_portile2 (~> 2.1.0)
pkg-config (~> 1.1.7)
numerizer (0.1.1)
- oauth (0.4.7)
+ oauth (0.5.1)
oauth2 (1.2.0)
faraday (>= 0.8, < 0.10)
jwt (~> 1.0)
@@ -867,7 +870,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.0)
- gitlab_git (~> 10.6.8)
+ gitlab_git (~> 10.7.0)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
@@ -881,6 +884,7 @@ DEPENDENCIES
html-pipeline (~> 1.11.0)
httparty (~> 0.13.3)
influxdb (~> 0.2)
+ jira-ruby (~> 1.1.2)
jquery-atwho-rails (~> 1.3.2)
jquery-rails (~> 4.1.0)
jquery-turbolinks (~> 2.1.0)
@@ -893,7 +897,7 @@ DEPENDENCIES
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
loofah (~> 2.0.3)
- mail_room (~> 0.8.1)
+ mail_room (~> 0.9.0)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
@@ -994,4 +998,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.13.2
+ 1.13.5
diff --git a/README.md b/README.md
index a6b30aff5a0..46492f4b9c8 100644
--- a/README.md
+++ b/README.md
@@ -56,6 +56,10 @@ There are various other options to install GitLab, please refer to the [installa
You can access a new installation with the login **`root`** and password **`5iveL!fe`**, after login you are required to set a unique password.
+## Contributing
+
+GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
+
## Install a development environment
To work on GitLab itself, we recommend setting up your development environment with [the GitLab Development Kit](https://gitlab.com/gitlab-org/gitlab-development-kit).
diff --git a/app/assets/javascripts/abuse_reports.js.es6 b/app/assets/javascripts/abuse_reports.js.es6
index 2fe46b9fd06..82e526ae0ef 100644
--- a/app/assets/javascripts/abuse_reports.js.es6
+++ b/app/assets/javascripts/abuse_reports.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
const MAX_MESSAGE_LENGTH = 500;
const MESSAGE_CELL_SELECTOR = '.abuse-reports .message';
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index f4f8cf04184..59ac9b9cef5 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Activities = (function() {
function Activities() {
@@ -24,9 +25,7 @@
var filter = sender.attr("id").split("_")[0];
$('.event-filter .active').removeClass("active");
- $.cookie("event_filter", filter, {
- path: gon.relative_url_root || '/'
- });
+ Cookies.set("event_filter", filter);
sender.closest('li').toggleClass("active");
};
diff --git a/app/assets/javascripts/admin.js b/app/assets/javascripts/admin.js
index f8460beb5d2..1ef340e4ca1 100644
--- a/app/assets/javascripts/admin.js
+++ b/app/assets/javascripts/admin.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Admin = (function() {
function Admin() {
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 56ec1489f89..7ebe1599fca 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Api = {
groupsPath: "/api/:version/groups.json",
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index 17cbfd0e66f..7dd9adac736 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -1,5 +1,6 @@
+/* eslint-disable */
// 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
+// Add new JavaScript 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.
@@ -11,13 +12,13 @@
/*= require jquery-ui/effect-highlight */
/*= require jquery-ui/sortable */
/*= require jquery_ujs */
-/*= require jquery.cookie */
/*= require jquery.endless-scroll */
/*= require jquery.highlight */
/*= require jquery.waitforimages */
/*= require jquery.atwho */
/*= require jquery.scrollTo */
/*= require jquery.turbolinks */
+/*= require js.cookie */
/*= require turbolinks */
/*= require autosave */
/*= require bootstrap/affix */
@@ -124,15 +125,11 @@
return str.replace(/<(?:.|\n)*?>/gm, '');
};
- window.unbindEvents = function() {
- return $(document).off('scroll');
- };
-
window.shiftWindow = function() {
return scrollBy(0, -100);
};
- document.addEventListener("page:fetch", unbindEvents);
+ document.addEventListener("page:fetch", gl.utils.cleanupBeforeFetch);
window.addEventListener("hashchange", shiftWindow);
@@ -149,6 +146,10 @@
$document = $(document);
$window = $(window);
$body = $('body');
+
+ // Set the default path for all cookies to GitLab's root directory
+ Cookies.defaults.path = gon.relative_url_root || '/';
+
gl.utils.preventDisabledButtons();
bootstrapBreakpoint = bp.getBreakpointSize();
$(".nav-sidebar").niceScroll({
@@ -187,6 +188,7 @@
// Close select2 on escape
});
// Initialize tooltips
+ $.fn.tooltip.Constructor.DEFAULTS.trigger = 'hover';
$body.tooltip({
selector: '.has-tooltip, [data-toggle="tooltip"]',
placement: function(_, el) {
diff --git a/app/assets/javascripts/aside.js b/app/assets/javascripts/aside.js
index 7b546e79ee0..c7eff27f971 100644
--- a/app/assets/javascripts/aside.js
+++ b/app/assets/javascripts/aside.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Aside = (function() {
function Aside() {
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index a9aec6e8ea4..ab09e4475e6 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Autosave = (function() {
function Autosave(field, key) {
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 44af1c135a0..8bdb0965f99 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.AwardsHandler = (function() {
const FROM_SENTENCE_REGEX = /(?:, and | and |, )/; //For separating lists produced by ruby's Array#toSentence
@@ -91,7 +92,7 @@
css = {
top: ($addBtn.offset().top + $addBtn.outerHeight()) + "px"
};
- if ((position != null) && position === 'right') {
+ if (position === 'right') {
css.left = (($addBtn.offset().left - $menu.outerWidth()) + 20) + "px";
$menu.addClass('is-aligned-right');
} else {
@@ -322,21 +323,18 @@
var frequentlyUsedEmojis;
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
frequentlyUsedEmojis.push(emoji);
- return $.cookie('frequently_used_emojis', frequentlyUsedEmojis.join(','), {
- path: gon.relative_url_root || '/',
- expires: 365
- });
+ Cookies.set('frequently_used_emojis', frequentlyUsedEmojis.join(','), { expires: 365 });
};
AwardsHandler.prototype.getFrequentlyUsedEmojis = function() {
var frequentlyUsedEmojis;
- frequentlyUsedEmojis = ($.cookie('frequently_used_emojis') || '').split(',');
+ frequentlyUsedEmojis = (Cookies.get('frequently_used_emojis') || '').split(',');
return _.compact(_.uniq(frequentlyUsedEmojis));
};
AwardsHandler.prototype.renderFrequentlyUsedBlock = function() {
var emoji, frequentlyUsedEmojis, i, len, ul;
- if ($.cookie('frequently_used_emojis')) {
+ if (Cookies.get('frequently_used_emojis')) {
frequentlyUsedEmojis = this.getFrequentlyUsedEmojis();
ul = $("<ul class='clearfix emoji-menu-list frequent-emojis'>");
for (i = 0, len = frequentlyUsedEmojis.length; i < len; i++) {
diff --git a/app/assets/javascripts/behaviors/autosize.js b/app/assets/javascripts/behaviors/autosize.js
index dc8ae601961..074378b9e52 100644
--- a/app/assets/javascripts/behaviors/autosize.js
+++ b/app/assets/javascripts/behaviors/autosize.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require jquery.ba-resize */
/*= require autosize */
diff --git a/app/assets/javascripts/behaviors/details_behavior.js b/app/assets/javascripts/behaviors/details_behavior.js
index 1df681a4816..a64cefb62bd 100644
--- a/app/assets/javascripts/behaviors/details_behavior.js
+++ b/app/assets/javascripts/behaviors/details_behavior.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
$(function() {
$("body").on("click", ".js-details-target", function() {
@@ -14,6 +15,11 @@
return $("body").on("click", ".js-details-expand", function(e) {
$(this).next('.js-details-content').removeClass("hide");
$(this).hide();
+
+ var truncatedItem = $(this).siblings('.js-details-short');
+ if (truncatedItem.length) {
+ truncatedItem.addClass("hide");
+ }
return e.preventDefault();
});
});
diff --git a/app/assets/javascripts/behaviors/quick_submit.js b/app/assets/javascripts/behaviors/quick_submit.js
index 54b7360ab41..7ff88ecdcaf 100644
--- a/app/assets/javascripts/behaviors/quick_submit.js
+++ b/app/assets/javascripts/behaviors/quick_submit.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Quick Submit behavior
//
// When a child field of a form with a `js-quick-submit` class receives a
diff --git a/app/assets/javascripts/behaviors/requires_input.js b/app/assets/javascripts/behaviors/requires_input.js
index 894034bdd54..4ac343f876c 100644
--- a/app/assets/javascripts/behaviors/requires_input.js
+++ b/app/assets/javascripts/behaviors/requires_input.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Requires Input behavior
//
// When called on a form with input fields with the `required` attribute, the
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index a6ce378d67a..05b213fe3fb 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function(w) {
$(function() {
// Toggle button. Show/hide content inside parent container.
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
index d6ea4f84f57..37531aaec9b 100644
--- a/app/assets/javascripts/blob/blob_ci_yaml.js.es6
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require blob/template_selector */
((global) => {
diff --git a/app/assets/javascripts/blob/blob_file_dropzone.js b/app/assets/javascripts/blob/blob_file_dropzone.js
index 8cca1aa9232..33fb4f8185c 100644
--- a/app/assets/javascripts/blob/blob_file_dropzone.js
+++ b/app/assets/javascripts/blob/blob_file_dropzone.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.BlobFileDropzone = (function() {
function BlobFileDropzone(form, method) {
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index cd746b05cf6..344fe5dcd94 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require blob/template_selector */
diff --git a/app/assets/javascripts/blob/blob_gitignore_selectors.js b/app/assets/javascripts/blob/blob_gitignore_selectors.js
index 4e9500428b2..9e992f7913c 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selectors.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selectors.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.BlobGitignoreSelectors = (function() {
function BlobGitignoreSelectors(opts) {
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 2701df3e6de..41a83a56146 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require blob/template_selector */
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
index 153ed457559..adeb8ba1318 100644
--- a/app/assets/javascripts/blob/blob_license_selectors.js.es6
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
class BlobLicenseSelectors {
constructor({ $dropdowns, editor }) {
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
index 4e309e480b0..5434a19bcec 100644
--- a/app/assets/javascripts/blob/template_selector.js.es6
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
class TemplateSelector {
constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
@@ -68,14 +69,10 @@
// To be implemented on the extending class
// e.g.
// Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- requestFileSuccess(file, { skipFocus, append } = {}) {
+ requestFileSuccess(file, { skipFocus } = {}) {
const oldValue = this.editor.getValue();
let newValue = file.content;
- if (append && oldValue.length && oldValue !== newValue) {
- newValue = oldValue + '\n\n' + newValue;
- }
-
this.editor.setValue(newValue, 1);
if (!skipFocus) this.editor.focus();
@@ -99,4 +96,3 @@
global.TemplateSelector = TemplateSelector;
})(window.gl || ( window.gl = {}));
-
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
index 2afef43f3d6..b801c10f168 100644
--- a/app/assets/javascripts/blob_edit/blob_edit_bundle.js
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 8db4f6a3b28..60840560dd3 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index d4f8f4b9420..efb22d38513 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require vue
//= require vue-resource
//= require Sortable
@@ -5,7 +6,9 @@
//= require_tree ./stores
//= require_tree ./services
//= require_tree ./mixins
+//= require_tree ./filters
//= require ./components/board
+//= require ./components/board_sidebar
//= require ./components/new_list_dropdown
//= require ./vue_resource_interceptor
@@ -22,7 +25,8 @@ $(() => {
gl.IssueBoardsApp = new Vue({
el: $boardApp,
components: {
- 'board': gl.issueBoards.Board
+ 'board': gl.issueBoards.Board,
+ 'board-sidebar': gl.issueBoards.BoardSidebar
},
data: {
state: Store.state,
@@ -30,9 +34,15 @@ $(() => {
endpoint: $boardApp.dataset.endpoint,
boardId: $boardApp.dataset.boardId,
disabled: $boardApp.dataset.disabled === 'true',
- issueLinkBase: $boardApp.dataset.issueLinkBase
+ issueLinkBase: $boardApp.dataset.issueLinkBase,
+ detailIssue: Store.detail
},
init: Store.create.bind(Store),
+ computed: {
+ detailIssueVisible () {
+ return Object.keys(this.detailIssue.issue).length;
+ }
+ },
created () {
gl.boardService = new BoardService(this.endpoint, this.boardId);
},
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index cacb36a897f..0e03d43872b 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require ./board_blank_state
//= require ./board_delete
//= require ./board_list
@@ -21,6 +22,7 @@
},
data () {
return {
+ detailIssue: Store.detail,
filters: Store.state.filters,
showIssueForm: false
};
@@ -32,6 +34,26 @@
this.list.getIssues(true);
},
deep: true
+ },
+ detailIssue: {
+ handler () {
+ if (!Object.keys(this.detailIssue.issue).length) return;
+
+ const issue = this.list.findIssue(this.detailIssue.issue.id);
+
+ if (issue) {
+ const boardsList = document.querySelectorAll('.boards-list')[0];
+ const right = (this.$el.offsetLeft + this.$el.offsetWidth) - boardsList.offsetWidth;
+ const left = boardsList.scrollLeft - this.$el.offsetLeft;
+
+ if (right - boardsList.scrollLeft > 0) {
+ boardsList.scrollLeft = right;
+ } else if (left > 0) {
+ boardsList.scrollLeft = this.$el.offsetLeft;
+ }
+ }
+ },
+ deep: true
}
},
methods: {
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index ff90f2d6d75..885553690d3 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(() => {
const Store = gl.issueBoards.BoardsStore;
diff --git a/app/assets/javascripts/boards/components/board_card.js.es6 b/app/assets/javascripts/boards/components/board_card.js.es6
index 4a7cfeaeab2..2f6c03e3538 100644
--- a/app/assets/javascripts/boards/components/board_card.js.es6
+++ b/app/assets/javascripts/boards/components/board_card.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -12,6 +13,17 @@
disabled: Boolean,
index: Number
},
+ data () {
+ return {
+ showDetail: false,
+ detailIssue: Store.detail
+ };
+ },
+ computed: {
+ issueDetailVisible () {
+ return this.detailIssue.issue && this.detailIssue.issue.id === this.issue.id;
+ }
+ },
methods: {
filterByLabel (label, e) {
let labelToggleText = label.title;
@@ -37,6 +49,29 @@
$('.labels-filter .dropdown-toggle-text').text(labelToggleText);
Store.updateFiltersUrl();
+ },
+ mouseDown () {
+ this.showDetail = true;
+ },
+ mouseMove () {
+ if (this.showDetail) {
+ this.showDetail = false;
+ }
+ },
+ showIssue (e) {
+ const targetTagName = e.target.tagName.toLowerCase();
+
+ if (targetTagName === 'a' || targetTagName === 'button') return;
+
+ if (this.showDetail) {
+ this.showDetail = false;
+
+ if (Store.detail.issue && Store.detail.issue.id === this.issue.id) {
+ Store.detail.issue = {};
+ } else {
+ Store.detail.issue = this.issue;
+ }
+ }
}
}
});
diff --git a/app/assets/javascripts/boards/components/board_delete.js.es6 b/app/assets/javascripts/boards/components/board_delete.js.es6
index 34653cd48ef..c45e1926c5c 100644
--- a/app/assets/javascripts/boards/components/board_delete.js.es6
+++ b/app/assets/javascripts/boards/components/board_delete.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 7022a29e818..34fc7694241 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require ./board_card
//= require ./board_new_issue
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
index a4fad422eca..7fc0bfd56f3 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js.es6
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -1,4 +1,7 @@
+/* eslint-disable */
(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
window.gl = window.gl || {};
gl.issueBoards.BoardNewIssue = Vue.extend({
@@ -27,13 +30,16 @@
const labels = this.list.label ? [this.list.label] : [];
const issue = new ListIssue({
title: this.title,
- labels
+ labels,
+ subscribed: true
});
this.list.newIssue(issue)
.then((data) => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
$(this.$els.submitButton).enable();
+
+ Store.detail.issue = issue;
})
.catch(() => {
// Need this because our jQuery very kindly disables buttons on ALL form submissions
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js.es6 b/app/assets/javascripts/boards/components/board_sidebar.js.es6
new file mode 100644
index 00000000000..4928320d015
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_sidebar.js.es6
@@ -0,0 +1,53 @@
+/* eslint-disable */
+(() => {
+ const Store = gl.issueBoards.BoardsStore;
+
+ window.gl = window.gl || {};
+ window.gl.issueBoards = window.gl.issueBoards || {};
+
+ gl.issueBoards.BoardSidebar = Vue.extend({
+ props: {
+ currentUser: Object
+ },
+ data() {
+ return {
+ detail: Store.detail,
+ issue: {}
+ };
+ },
+ computed: {
+ showSidebar () {
+ return Object.keys(this.issue).length;
+ }
+ },
+ watch: {
+ detail: {
+ handler () {
+ this.issue = this.detail.issue;
+ },
+ deep: true
+ },
+ issue () {
+ if (this.showSidebar) {
+ this.$nextTick(() => {
+ $('.right-sidebar').getNiceScroll(0).doScrollTop(0, 0);
+ $('.right-sidebar').getNiceScroll().resize();
+ });
+ }
+ }
+ },
+ methods: {
+ closeSidebar () {
+ this.detail.issue = {};
+ }
+ },
+ ready () {
+ new IssuableContext(this.currentUser);
+ new MilestoneSelect();
+ new gl.DueDateSelectors();
+ new LabelsSelect();
+ new Sidebar();
+ new Subscription('.subscription');
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 6ccd83e2d84..fe1a6dc7ea0 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
$(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -32,6 +33,7 @@ $(() => {
},
filterable: true,
selectable: true,
+ multiSelect: true,
clicked (label, $el, e) {
e.preventDefault();
diff --git a/app/assets/javascripts/boards/filters/due_date_filters.js.es6 b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
new file mode 100644
index 00000000000..9eceac4eddd
--- /dev/null
+++ b/app/assets/javascripts/boards/filters/due_date_filters.js.es6
@@ -0,0 +1,5 @@
+/* eslint-disable */
+Vue.filter('due-date', (value) => {
+ const date = new Date(value);
+ return $.datepicker.formatDate('M d, yy', date);
+});
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index f629d45c587..db9a5a8e40a 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -21,8 +22,8 @@
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
- filter: '.has-tooltip, .btn',
- delay: gl.issueBoards.touchEnabled ? 100 : 0,
+ filter: '.board-delete, .btn',
+ delay: gl.issueBoards.touchEnabled ? 100 : 50,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
onStart: gl.issueBoards.onStart,
diff --git a/app/assets/javascripts/boards/models/issue.js.es6 b/app/assets/javascripts/boards/models/issue.js.es6
index eb082103de9..21d735e8231 100644
--- a/app/assets/javascripts/boards/models/issue.js.es6
+++ b/app/assets/javascripts/boards/models/issue.js.es6
@@ -1,14 +1,21 @@
+/* eslint-disable */
class ListIssue {
constructor (obj) {
this.id = obj.iid;
this.title = obj.title;
this.confidential = obj.confidential;
+ this.dueDate = obj.due_date;
+ this.subscribed = obj.subscribed;
this.labels = [];
if (obj.assignee) {
this.assignee = new ListUser(obj.assignee);
}
+ if (obj.milestone) {
+ this.milestone = new ListMilestone(obj.milestone);
+ }
+
obj.labels.forEach((label) => {
this.labels.push(new ListLabel(label));
});
@@ -41,4 +48,21 @@ class ListIssue {
getLists () {
return gl.issueBoards.BoardsStore.state.lists.filter( list => list.findIssue(this.id) );
}
+
+ update (url) {
+ const data = {
+ issue: {
+ milestone_id: this.milestone ? this.milestone.id : null,
+ due_date: this.dueDate,
+ assignee_id: this.assignee ? this.assignee.id : null,
+ label_ids: this.labels.map( (label) => label.id )
+ }
+ };
+
+ if (!data.issue.label_ids.length) {
+ data.issue.label_ids = [''];
+ }
+
+ return Vue.http.patch(url, data);
+ }
}
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index 583829552cd..0910fe9a854 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
class ListLabel {
constructor (obj) {
this.id = obj.id;
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 5d0a561cdba..b331a26fed5 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
class List {
constructor (obj) {
this.id = obj.id;
diff --git a/app/assets/javascripts/boards/models/milestone.js.es6 b/app/assets/javascripts/boards/models/milestone.js.es6
new file mode 100644
index 00000000000..a48969e19c9
--- /dev/null
+++ b/app/assets/javascripts/boards/models/milestone.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable */
+class ListMilestone {
+ constructor (obj) {
+ this.id = obj.id;
+ this.title = obj.title;
+ }
+}
diff --git a/app/assets/javascripts/boards/models/user.js.es6 b/app/assets/javascripts/boards/models/user.js.es6
index 904b3a68507..583a973fc46 100644
--- a/app/assets/javascripts/boards/models/user.js.es6
+++ b/app/assets/javascripts/boards/models/user.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
class ListUser {
constructor (user) {
this.id = user.id;
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index b9c91cbf31e..f59a2ed7937 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -1,7 +1,6 @@
+/* eslint-disable */
class BoardService {
constructor (root, boardId) {
- Vue.http.options.root = root;
-
this.lists = Vue.resource(`${root}/${boardId}/lists{/id}`, {}, {
generate: {
method: 'POST',
diff --git a/app/assets/javascripts/boards/stores/boards_store.js.es6 b/app/assets/javascripts/boards/stores/boards_store.js.es6
index bd07ee0c161..175e034afed 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js.es6
+++ b/app/assets/javascripts/boards/stores/boards_store.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(() => {
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
@@ -5,6 +6,9 @@
gl.issueBoards.BoardsStore = {
disabled: false,
state: {},
+ detail: {
+ issue: {}
+ },
moving: {
issue: {},
list: {}
@@ -58,12 +62,13 @@
removeBlankState () {
this.removeList('blank');
- $.cookie('issue_board_welcome_hidden', 'true', {
- expires: 365 * 10
+ Cookies.set('issue_board_welcome_hidden', 'true', {
+ expires: 365 * 10,
+ path: ''
});
},
welcomeIsHidden () {
- return $.cookie('issue_board_welcome_hidden') === 'true';
+ return Cookies.get('issue_board_welcome_hidden') === 'true';
},
removeList (id, type = 'blank') {
const list = this.findList('id', id, type);
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
index 75f8b730195..039ca491cf5 100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function () {
'use strict';
diff --git a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6 b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
index b5ff3a81ed5..80f137ca12e 100644
--- a/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
+++ b/app/assets/javascripts/boards/vue_resource_interceptor.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
Vue.http.interceptors.push((request, next) => {
Vue.activeResources = Vue.activeResources ? Vue.activeResources + 1 : 1;
diff --git a/app/assets/javascripts/breakpoints.js b/app/assets/javascripts/breakpoints.js
index 5fef9725178..5d4d23e26c6 100644
--- a/app/assets/javascripts/breakpoints.js
+++ b/app/assets/javascripts/breakpoints.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Breakpoints = (function() {
var BreakpointInstance, instance;
diff --git a/app/assets/javascripts/broadcast_message.js b/app/assets/javascripts/broadcast_message.js
index fceeff36728..576f4c76c1e 100644
--- a/app/assets/javascripts/broadcast_message.js
+++ b/app/assets/javascripts/broadcast_message.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
$(function() {
var previewPath;
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index f4c387a1a05..12e653f4122 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/build_artifacts.js b/app/assets/javascripts/build_artifacts.js
index f345ba0abe6..49f84581650 100644
--- a/app/assets/javascripts/build_artifacts.js
+++ b/app/assets/javascripts/build_artifacts.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.BuildArtifacts = (function() {
function BuildArtifacts() {
diff --git a/app/assets/javascripts/build_variables.js.es6 b/app/assets/javascripts/build_variables.js.es6
index 8d3e29794a1..0ecd20bc11e 100644
--- a/app/assets/javascripts/build_variables.js.es6
+++ b/app/assets/javascripts/build_variables.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
$(function(){
$('.reveal-variables').off('click').on('click',function(){
$('.js-build').toggle().niceScroll();
diff --git a/app/assets/javascripts/commit.js b/app/assets/javascripts/commit.js
index 23cf5b519f4..fac5b4f17da 100644
--- a/app/assets/javascripts/commit.js
+++ b/app/assets/javascripts/commit.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Commit = (function() {
function Commit() {
diff --git a/app/assets/javascripts/commit/file.js b/app/assets/javascripts/commit/file.js
index be24ee56aad..16d63729d31 100644
--- a/app/assets/javascripts/commit/file.js
+++ b/app/assets/javascripts/commit/file.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.CommitFile = (function() {
function CommitFile(file) {
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index e893491b19b..ffddce1297b 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ImageFile = (function() {
var prepareFrames;
diff --git a/app/assets/javascripts/commits.js b/app/assets/javascripts/commits.js
index 9132089adcd..c765d233831 100644
--- a/app/assets/javascripts/commits.js
+++ b/app/assets/javascripts/commits.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.CommitsList = (function() {
function CommitsList() {}
diff --git a/app/assets/javascripts/compare.js b/app/assets/javascripts/compare.js
index 342ac0e8e69..b3f769d4129 100644
--- a/app/assets/javascripts/compare.js
+++ b/app/assets/javascripts/compare.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Compare = (function() {
function Compare(opts) {
diff --git a/app/assets/javascripts/compare_autocomplete.js.es6 b/app/assets/javascripts/compare_autocomplete.js.es6
index 9a2082d97e0..bd980f87e72 100644
--- a/app/assets/javascripts/compare_autocomplete.js.es6
+++ b/app/assets/javascripts/compare_autocomplete.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.CompareAutocomplete = (function() {
function CompareAutocomplete() {
diff --git a/app/assets/javascripts/confirm_danger_modal.js b/app/assets/javascripts/confirm_danger_modal.js
index 708ab08ffac..230a1b95c52 100644
--- a/app/assets/javascripts/confirm_danger_modal.js
+++ b/app/assets/javascripts/confirm_danger_modal.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ConfirmDangerModal = (function() {
function ConfirmDangerModal(form, text) {
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index e23bda2fa4e..7808d7fe313 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require clipboard */
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index c5f8c29242d..f20580b1279 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function (w) {
class CreateLabelDropdown {
constructor ($el, namespacePath, projectPath) {
diff --git a/app/assets/javascripts/cycle_analytics.js.es6 b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
index 20791bab942..331f0209888 100644
--- a/app/assets/javascripts/cycle_analytics.js.es6
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require vue
((global) => {
@@ -6,7 +7,7 @@
const store = gl.cycleAnalyticsStore = {
isLoading: true,
hasError: false,
- isHelpDismissed: $.cookie(COOKIE_NAME),
+ isHelpDismissed: Cookies.get(COOKIE_NAME),
analytics: {}
};
@@ -75,9 +76,7 @@
dismissLanding() {
store.isHelpDismissed = true;
- $.cookie(COOKIE_NAME, true, {
- path: gon.relative_url_root || '/'
- });
+ Cookies.set(COOKIE_NAME, true);
}
initDropdown() {
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index 8086c10ad6b..4ddafff428f 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Diff = (function() {
var UNFOLD_COUNT;
diff --git a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
index 48bc7d77805..29a12a2395b 100644
--- a/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/comment_resolve_btn.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
w.CommentAndResolveBtn = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6 b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
index ad80d1118df..983e554b9c1 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(() => {
JumpToDiscussion = Vue.extend({
mixins: [DiscussionMixins],
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index cdedfd1af15..bcc052c7c8c 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
w.ResolveBtn = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
index 9e383b14a3e..24a99e23132 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_count.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
w.ResolveCount = Vue.extend({
mixins: [DiscussionMixins],
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index 0a617034502..060034f049b 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
w.ResolveDiscussionBtn = Vue.extend({
props: {
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6 b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
index 22d9cf6c857..6149bfd052a 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require vue
//= require vue-resource
//= require_directory ./models
diff --git a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6 b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
index a05f885201d..7a929017f36 100644
--- a/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/mixins/discussion.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
w.DiscussionMixins = {
computed: {
diff --git a/app/assets/javascripts/diff_notes/models/discussion.js.es6 b/app/assets/javascripts/diff_notes/models/discussion.js.es6
index 488714e4870..439f55520ef 100644
--- a/app/assets/javascripts/diff_notes/models/discussion.js.es6
+++ b/app/assets/javascripts/diff_notes/models/discussion.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
class DiscussionModel {
constructor (discussionId) {
this.id = discussionId;
diff --git a/app/assets/javascripts/diff_notes/models/note.js.es6 b/app/assets/javascripts/diff_notes/models/note.js.es6
index f2d2d389c38..d0541b02632 100644
--- a/app/assets/javascripts/diff_notes/models/note.js.es6
+++ b/app/assets/javascripts/diff_notes/models/note.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
class NoteModel {
constructor (discussionId, noteId, canResolve, resolved, resolved_by) {
this.discussionId = discussionId;
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index 2a55f739b31..86953ce7ffb 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
class ResolveServiceClass {
constructor() {
diff --git a/app/assets/javascripts/diff_notes/stores/comments.js.es6 b/app/assets/javascripts/diff_notes/stores/comments.js.es6
index 69522e1dac5..f42ca406bb1 100644
--- a/app/assets/javascripts/diff_notes/stores/comments.js.es6
+++ b/app/assets/javascripts/diff_notes/stores/comments.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
w.CommentsStore = {
state: {},
diff --git a/app/assets/javascripts/dispatcher.js.es6 b/app/assets/javascripts/dispatcher.js.es6
index a1fe57562fa..ff8b8f6d0ae 100644
--- a/app/assets/javascripts/dispatcher.js.es6
+++ b/app/assets/javascripts/dispatcher.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var Dispatcher;
diff --git a/app/assets/javascripts/dropzone_input.js b/app/assets/javascripts/dropzone_input.js
index 4a6fea929c7..1a0aa9757ba 100644
--- a/app/assets/javascripts/dropzone_input.js
+++ b/app/assets/javascripts/dropzone_input.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require preview_markdown */
diff --git a/app/assets/javascripts/due_date_select.js.es6 b/app/assets/javascripts/due_date_select.js.es6
index 41925fcc8e3..fd7f961aab9 100644
--- a/app/assets/javascripts/due_date_select.js.es6
+++ b/app/assets/javascripts/due_date_select.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function(global) {
class DueDateSelect {
constructor({ $dropdown, $loading } = {}) {
@@ -41,7 +42,12 @@
defaultDate: $("input[name='" + this.fieldName + "']").val(),
altField: "input[name='" + this.fieldName + "']",
onSelect: () => {
- return this.saveDueDate(true);
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = $(`input[name='${this.fieldName}']`).val();
+ this.updateIssueBoardIssue();
+ } else {
+ return this.saveDueDate(true);
+ }
}
});
}
@@ -49,8 +55,14 @@
initRemoveDueDate() {
this.$block.on('click', '.js-remove-due-date', (e) => {
e.preventDefault();
- $("input[name='" + this.fieldName + "']").val('');
- return this.saveDueDate(false);
+
+ if (this.$dropdown.hasClass('js-issue-boards-due-date')) {
+ gl.issueBoards.BoardsStore.detail.issue.dueDate = '';
+ this.updateIssueBoardIssue();
+ } else {
+ $("input[name='" + this.fieldName + "']").val('');
+ return this.saveDueDate(false);
+ }
});
}
@@ -83,6 +95,18 @@
this.datePayload = datePayload;
}
+ updateIssueBoardIssue () {
+ this.$loading.fadeIn();
+ this.$dropdown.trigger('loading.gl.dropdown');
+ this.$selectbox.hide();
+ this.$value.css('display', '');
+
+ gl.issueBoards.BoardsStore.detail.issue.update(this.$dropdown.attr('data-issue-update'))
+ .then(() => {
+ this.$loading.fadeOut();
+ });
+ }
+
submitSelectedDate(isDropdown) {
return $.ajax({
type: 'PUT',
diff --git a/app/assets/javascripts/extensions/array.js b/app/assets/javascripts/extensions/array.js
index 24f9e00097c..4c9e219aa43 100644
--- a/app/assets/javascripts/extensions/array.js
+++ b/app/assets/javascripts/extensions/array.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
Array.prototype.first = function() {
return this[0];
}
diff --git a/app/assets/javascripts/extensions/element.js.es6 b/app/assets/javascripts/extensions/element.js.es6
new file mode 100644
index 00000000000..c74fc9ad074
--- /dev/null
+++ b/app/assets/javascripts/extensions/element.js.es6
@@ -0,0 +1,7 @@
+/* eslint-disable */
+Element.prototype.matches = Element.prototype.matches || Element.prototype.msMatches;
+
+Element.prototype.closest = function closest(selector, selectedElement = this) {
+ if (!selectedElement) return;
+ return selectedElement.matches(selector) ? selectedElement : Element.prototype.closest(selector, selectedElement.parentElement);
+};
diff --git a/app/assets/javascripts/extensions/jquery.js b/app/assets/javascripts/extensions/jquery.js
index 4978e24949c..623a80b7053 100644
--- a/app/assets/javascripts/extensions/jquery.js
+++ b/app/assets/javascripts/extensions/jquery.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Disable an element and add the 'disabled' Bootstrap class
(function() {
$.fn.extend({
diff --git a/app/assets/javascripts/files_comment_button.js b/app/assets/javascripts/files_comment_button.js
index 3fb3b1a8b51..732136f1f2c 100644
--- a/app/assets/javascripts/files_comment_button.js
+++ b/app/assets/javascripts/files_comment_button.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index c8a02d6fa15..46e272c3311 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Flash = (function() {
var hideFlash;
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index 845313b6b38..31df51ac03a 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Creates the variables for setting up GFM auto-completion
(function() {
if (window.GitLab == null) {
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 53762f2965c..98e43c4d088 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var GitLabDropdown, GitLabDropdownFilter, GitLabDropdownRemote,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
@@ -208,7 +209,7 @@
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
function GitLabDropdown(el1, options) {
- var ref, ref1, ref2, ref3, searchFields, selector, self;
+ var searchFields, selector, self;
this.el = el1;
this.options = options;
this.updateLabel = bind(this.updateLabel, this);
@@ -219,7 +220,11 @@
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;
+ this.filterInput = this.options.filterInput || this.getElement(FILTER_INPUT);
+ this.highlight = !!this.options.highlight
+ this.filterInputBlur = this.options.filterInputBlur != null
+ ? this.options.filterInputBlur
+ : true;
// If no input is passed create a default one
self = this;
// If selector was passed
@@ -234,6 +239,7 @@
this.fullData = this.options.data;
currentIndex = -1;
this.parseData(this.options.data);
+ this.focusTextInput();
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
dataType: this.options.dataType,
@@ -242,6 +248,7 @@
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
+ _this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input) {
return _this.filter.input.trigger('input');
}
@@ -418,7 +425,9 @@
var $target;
if (this.options.multiSelect) {
$target = $(e.target);
- if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
+ if ($target && !$target.hasClass('dropdown-menu-close') &&
+ !$target.hasClass('dropdown-menu-close-icon') &&
+ !$target.data('is-link')) {
e.stopPropagation();
return false;
} else {
@@ -445,9 +454,8 @@
contentHtml = $('.dropdown-content', this.dropdown).html();
if (this.remote && contentHtml === "") {
this.remote.execute();
- }
- if (this.options.filterable) {
- this.filterInput.focus();
+ } else {
+ this.focusTextInput();
}
if (this.options.showMenuAbove) {
@@ -549,6 +557,8 @@
value = this.options.id ? this.options.id(data) : data.id;
fieldName = this.options.fieldName;
+ if (value) { value = value.toString().replace(/'/g, '\\\'') };
+
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
selected = true;
@@ -620,8 +630,21 @@
selectedObject = this.renderedData[selectedIndex];
}
}
+
+ if (this.options.vue) {
+ if (el.hasClass(ACTIVE_CLASS)) {
+ el.removeClass(ACTIVE_CLASS);
+ } else {
+ el.addClass(ACTIVE_CLASS);
+ }
+
+ return selectedObject;
+ }
+
field = [];
- value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
+ value = this.options.id
+ ? this.options.id(selectedObject, el)
+ : selectedObject.id;
if (isInput) {
field = $(this.el);
} else if(value) {
@@ -669,6 +692,10 @@
return selectedObject;
};
+ GitLabDropdown.prototype.focusTextInput = function() {
+ if (this.options.filterable) { this.filterInput.focus() }
+ }
+
GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
// Create hidden input for form
diff --git a/app/assets/javascripts/gl_field_errors.js.es6 b/app/assets/javascripts/gl_field_errors.js.es6
index 8e8f9f29ab3..be6c3ec274f 100644
--- a/app/assets/javascripts/gl_field_errors.js.es6
+++ b/app/assets/javascripts/gl_field_errors.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
/*
* This class overrides the browser's validation error bubbles, displaying custom
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index 2703adc0705..742807d93ad 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.GLForm = (function() {
function GLForm(form) {
diff --git a/app/assets/javascripts/graphs/graphs_bundle.js b/app/assets/javascripts/graphs/graphs_bundle.js
index 4886da9f21f..e103748d499 100644
--- a/app/assets/javascripts/graphs/graphs_bundle.js
+++ b/app/assets/javascripts/graphs/graphs_bundle.js
@@ -1,5 +1,6 @@
+/* eslint-disable */
// 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
+// Add new JavaScript 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.
diff --git a/app/assets/javascripts/graphs/stat_graph.js b/app/assets/javascripts/graphs/stat_graph.js
index f041980bc19..b796a9abb49 100644
--- a/app/assets/javascripts/graphs/stat_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.StatGraph = (function() {
function StatGraph() {}
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 927d241b357..818bff0c413 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require d3 */
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index 7d9d4d7c679..dea26a3f1e1 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require d3 */
@@ -29,8 +30,7 @@
ContributorsGraph.set_y_domain = function(data) {
return ContributorsGraph.prototype.y_domain = [
0, d3.max(data, function(d) {
- var ref, ref1;
- return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+ return d.commits = d.commits || d.additions || d.deletions;
})
];
};
@@ -44,8 +44,7 @@
ContributorsGraph.init_y_domain = function(data) {
return ContributorsGraph.prototype.y_domain = [
0, d3.max(data, function(d) {
- var ref, ref1;
- return d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
+ return d.commits = d.commits || d.additions || d.deletions;
})
];
};
@@ -147,9 +146,8 @@
return this.area = d3.svg.area().x(function(d) {
return x(d.date);
}).y0(this.height).y1(function(d) {
- var ref, ref1, xa;
- xa = d.commits = (ref = (ref1 = d.commits) != null ? ref1 : d.additions) != null ? ref : d.deletions;
- return y(xa);
+ d.commits = d.commits || d.additions || d.deletions;
+ return y(d.commits);
}).interpolate("basis");
};
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_util.js b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
index 0d240bed8b6..362a77e868f 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_util.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_util.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
window.ContributorsStatGraphUtil = {
parse_log: function(log) {
diff --git a/app/assets/javascripts/group_avatar.js b/app/assets/javascripts/group_avatar.js
index c28ce86d7af..774477dc7a9 100644
--- a/app/assets/javascripts/group_avatar.js
+++ b/app/assets/javascripts/group_avatar.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.GroupAvatar = (function() {
function GroupAvatar() {
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 5f06186504b..b275620c799 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var slice = [].slice;
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
new file mode 100644
index 00000000000..81fcaf06430
--- /dev/null
+++ b/app/assets/javascripts/header.js
@@ -0,0 +1,10 @@
+/* eslint-disable */
+(function() {
+
+ $(document).on('todo:toggle', function(e, count) {
+ var $todoPendingCount = $('.todos-pending-count');
+ $todoPendingCount.text(gl.text.addDelimiter(count));
+ $todoPendingCount.toggleClass('hidden', count === 0);
+ });
+
+})();
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 4aced1e618f..c53f7c88aa2 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ImporterStatus = (function() {
function ImporterStatus(jobs_url, import_url) {
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 57f7e4ef230..8fc498be27d 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var issuable_created;
@@ -15,16 +16,61 @@
return Issuable.labelRow = _.template('<% _.each(labels, function(label){ %> <span class="label-row btn-group" role="group" aria-label="<%- label.title %>" style="color: <%- label.text_color %>;"> <a href="#" class="btn btn-transparent has-tooltip" style="background-color: <%- label.color %>;" title="<%- label.description %>" data-container="body"> <%- label.title %> </a> <button type="button" class="btn btn-transparent label-remove js-label-filter-remove" style="background-color: <%- label.color %>;" data-label="<%- label.title %>"> <i class="fa fa-times"></i> </button> </span> <% }); %>');
},
initSearch: function() {
+ const $searchInput = $('#issuable_search');
+
+ Issuable.initSearchState($searchInput);
+
// `immediate` param set to false debounces on the `trailing` edge, lets user finish typing
- const debouncedExecSearch = _.debounce(Issuable.executeSearch, 500, false);
+ const debouncedExecSearch = _.debounce(Issuable.executeSearch, 1000, false);
- $('#issuable_search').off('keyup').on('keyup', debouncedExecSearch);
+ $searchInput.off('keyup').on('keyup', debouncedExecSearch);
// ensures existing filters are preserved when manually submitted
- $('#issue_search_form').on('submit', (e) => {
+ $('#issuable_search_form').on('submit', (e) => {
e.preventDefault();
debouncedExecSearch(e);
});
+
+ },
+ initSearchState: function($searchInput) {
+ const currentSearchVal = $searchInput.val();
+
+ Issuable.searchState = {
+ elem: $searchInput,
+ current: currentSearchVal
+ };
+
+ Issuable.maybeFocusOnSearch();
+ },
+ accessSearchPristine: function(set) {
+ // store reference to previous value to prevent search on non-mutating keyup
+ const state = Issuable.searchState;
+ const currentSearchVal = state.elem.val();
+
+ if (set) {
+ state.current = currentSearchVal;
+ } else {
+ return state.current === currentSearchVal;
+ }
+ },
+ maybeFocusOnSearch: function() {
+ const currentSearchVal = Issuable.searchState.current;
+ if (currentSearchVal && currentSearchVal !== '') {
+ const queryLength = currentSearchVal.length;
+ const $searchInput = Issuable.searchState.elem;
+
+ /* The following ensures that the cursor is initially placed at
+ * the end of search input when focus is applied. It accounts
+ * for differences in browser implementations of `setSelectionRange`
+ * and cursor placement for elements in focus.
+ */
+ $searchInput.focus();
+ if ($searchInput.setSelectionRange) {
+ $searchInput.setSelectionRange(queryLength, queryLength);
+ } else {
+ $searchInput.val(currentSearchVal);
+ }
+ }
},
executeSearch: function(e) {
const $search = $('#issuable_search');
@@ -32,6 +78,11 @@
const $searchValue = $search.val();
const $filtersForm = $('.js-filter-form');
const $input = $(`input[name='${$searchName}']`, $filtersForm);
+ const isPristine = Issuable.accessSearchPristine();
+
+ if (isPristine) {
+ return;
+ }
if (!$input.length) {
$filtersForm.append(`<input type='hidden' name='${$searchName}' value='${_.escape($searchValue)}'/>`);
diff --git a/app/assets/javascripts/issuable_context.js b/app/assets/javascripts/issuable_context.js
index 8147e83ffe8..fae49ee6144 100644
--- a/app/assets/javascripts/issuable_context.js
+++ b/app/assets/javascripts/issuable_context.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.IssuableContext = (function() {
function IssuableContext(currentUser) {
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index b7f92ae9883..849b45756ee 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -44,8 +45,8 @@
};
IssuableForm.prototype.handleSubmit = function() {
- var ref, ref1;
- if (((ref = parseInt((ref1 = this.issueMoveField) != null ? ref1.val() : void 0)) != null ? ref : 0) > 0) {
+ var fieldId = (this.issueMoveField != null) ? this.issueMoveField.val() : null;
+ if ((parseInt(fieldId) || 0) > 0) {
if (!confirm(this.issueMoveConfirmMsg)) {
return false;
}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index 261bf6137c2..67ace697936 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require flash */
/*= require jquery.waitforimages */
@@ -94,7 +95,11 @@
return $.ajax({
type: 'PATCH',
url: $('form.js-issuable-update').attr('action'),
- data: patchData
+ data: patchData,
+ success: function(issue) {
+ document.querySelector('#task_status').innerText = issue.task_status;
+ document.querySelector('#task_status_short').innerText = issue.task_status_short;
+ }
});
// TODO (rspeicher): Make the issue description inline-editable like a note so
// that we can re-use its form here
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index 076e3972944..d7262e5eb74 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.IssueStatusSelect = (function() {
function IssueStatusSelect() {
diff --git a/app/assets/javascripts/issues_bulk_assignment.js.es6 b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 0808f538f01..9697fb33566 100644
--- a/app/assets/javascripts/issues_bulk_assignment.js.es6
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
class IssuableBulkActions {
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
index bc68e53504f..175623e7448 100644
--- a/app/assets/javascripts/label_manager.js.es6
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
class LabelManager {
diff --git a/app/assets/javascripts/labels.js b/app/assets/javascripts/labels.js
index cb16e2ba814..3033e8ca5c2 100644
--- a/app/assets/javascripts/labels.js
+++ b/app/assets/javascripts/labels.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index b4f6e70f694..c334e3e0c02 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.LabelsSelect = (function() {
function LabelsSelect() {
@@ -22,7 +23,7 @@
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
$block = $selectbox.closest('.block');
- $form = $dropdown.closest('form');
+ $form = $dropdown.closest('form, .js-issuable-update');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
@@ -317,6 +318,7 @@
}
},
multiSelect: $dropdown.hasClass('js-multiselect'),
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown();
@@ -334,7 +336,7 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
}
@@ -362,6 +364,30 @@
else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
}
+ else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if ($el.hasClass('is-active')) {
+ gl.issueBoards.BoardsStore.detail.issue.labels.push(new ListLabel({
+ id: label.id,
+ title: label.title,
+ color: label.color[0],
+ textColor: '#fff'
+ }));
+ }
+ else {
+ var labels = gl.issueBoards.BoardsStore.detail.issue.labels;
+ labels = labels.filter(function (selectedLabel) {
+ return selectedLabel.id !== label.id;
+ });
+ gl.issueBoards.BoardsStore.detail.issue.labels = labels;
+ }
+
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $loading.fadeOut();
+ });
+ }
else {
if ($dropdown.hasClass('js-multiselect')) {
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index 8e2fc0d1479..6b4edf02f4d 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var hideEndFade;
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
index 4cdf99cae72..b1718e89d3d 100644
--- a/app/assets/javascripts/lib/ace.js
+++ b/app/assets/javascripts/lib/ace.js
@@ -1,2 +1,3 @@
+/* eslint-disable */
/*= require ace-rails-ap */
/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/lib/chart.js b/app/assets/javascripts/lib/chart.js
index d9b07c10a49..e1dfdae97de 100644
--- a/app/assets/javascripts/lib/chart.js
+++ b/app/assets/javascripts/lib/chart.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require Chart */
diff --git a/app/assets/javascripts/lib/cropper.js b/app/assets/javascripts/lib/cropper.js
index a88e640f298..155e30cc462 100644
--- a/app/assets/javascripts/lib/cropper.js
+++ b/app/assets/javascripts/lib/cropper.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require cropper */
diff --git a/app/assets/javascripts/lib/d3.js b/app/assets/javascripts/lib/d3.js
index ee1baf54803..0c9c2787077 100644
--- a/app/assets/javascripts/lib/d3.js
+++ b/app/assets/javascripts/lib/d3.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require d3 */
diff --git a/app/assets/javascripts/lib/raphael.js b/app/assets/javascripts/lib/raphael.js
index 6df427bc2b1..cc445db274b 100644
--- a/app/assets/javascripts/lib/raphael.js
+++ b/app/assets/javascripts/lib/raphael.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require raphael */
/*= require g.raphael */
diff --git a/app/assets/javascripts/lib/utils/animate.js b/app/assets/javascripts/lib/utils/animate.js
index d36efdabc93..a68edab2aad 100644
--- a/app/assets/javascripts/lib/utils/animate.js
+++ b/app/assets/javascripts/lib/utils/animate.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
(function(w) {
if (w.gl == null) {
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index b170e26eebf..21efe2d76dd 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
(function(w) {
var base;
@@ -43,6 +44,14 @@
parser.href = url;
return parser;
};
+
+ gl.utils.cleanupBeforeFetch = function() {
+ // Unbind scroll events
+ $(document).off('scroll');
+ // Close any open tooltips
+ $('.has-tooltip, [data-toggle="tooltip"]').tooltip('destroy');
+ };
+
return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor;
if (!time) {
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 8fdf4646cd8..59e526ed623 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
(function(w) {
var base;
diff --git a/app/assets/javascripts/lib/utils/jquery.timeago.js b/app/assets/javascripts/lib/utils/jquery.timeago.js
index cc17aa7d3d1..de76cdd2ea7 100644
--- a/app/assets/javascripts/lib/utils/jquery.timeago.js
+++ b/app/assets/javascripts/lib/utils/jquery.timeago.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/**
* Timeago is a jQuery plugin that makes it easy to support automatically
* updating fuzzy timestamps (e.g. "4 minutes ago" or "about 1 day ago").
diff --git a/app/assets/javascripts/lib/utils/notify.js b/app/assets/javascripts/lib/utils/notify.js
index 5b338b00d76..dafc006d2e5 100644
--- a/app/assets/javascripts/lib/utils/notify.js
+++ b/app/assets/javascripts/lib/utils/notify.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
(function(w) {
var notificationGranted, notifyMe, notifyPermissions;
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index d761a844be9..98f9815ff05 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
(function(w) {
var base;
@@ -7,6 +8,9 @@
if ((base = w.gl).text == null) {
base.text = {};
}
+ gl.text.addDelimiter = function(text) {
+ return text ? text.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ",") : text;
+ }
gl.text.randomString = function() {
return Math.random().toString(36).substring(7);
};
diff --git a/app/assets/javascripts/lib/utils/type_utility.js b/app/assets/javascripts/lib/utils/type_utility.js
index dc30babd645..4fd1e3fc1d3 100644
--- a/app/assets/javascripts/lib/utils/type_utility.js
+++ b/app/assets/javascripts/lib/utils/type_utility.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
(function(w) {
var base;
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index b8d52becb3f..44a66a915e3 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
(function(w) {
var base;
diff --git a/app/assets/javascripts/line_highlighter.js b/app/assets/javascripts/line_highlighter.js
index 93daea1dce7..ea5a60bb78e 100644
--- a/app/assets/javascripts/line_highlighter.js
+++ b/app/assets/javascripts/line_highlighter.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// LineHighlighter
//
// Handles single- and multi-line selection and highlight for blob views.
diff --git a/app/assets/javascripts/logo.js b/app/assets/javascripts/logo.js
index 7d8eef1b495..d4f86534f0c 100644
--- a/app/assets/javascripts/logo.js
+++ b/app/assets/javascripts/logo.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
Turbolinks.enableProgressBar();
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
index e1532fd9ec4..0bd90c57396 100644
--- a/app/assets/javascripts/member_expiration_date.js
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
// Add datepickers to all `js-access-expiration-date` elements. If those elements are
// children of an element with the `clearable-input` class, and have a sibling
diff --git a/app/assets/javascripts/members.js.es6 b/app/assets/javascripts/members.js.es6
index 2bdd0f7a637..371abd09e78 100644
--- a/app/assets/javascripts/members.js.es6
+++ b/app/assets/javascripts/members.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((w) => {
w.gl = w.gl || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6 b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
index 5012bdfe997..6da3942ea52 100644
--- a/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/diff_file_editor.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
index b4be1c8988d..23c4618af70 100644
--- a/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/inline_conflict_lines.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
index 8b0a8ab2073..797850262cc 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_line.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6 b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
index eb4cc6a9dac..1b3e9901f1e 100644
--- a/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
+++ b/app/assets/javascripts/merge_conflicts/components/parallel_conflict_lines.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
index da2fb8b1323..8a7519b0786 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_service.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
index 5c5c65f29d4..f94e51e783c 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js.es6
@@ -1,7 +1,8 @@
+/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
- const diffViewType = $.cookie('diff_view');
+ const diffViewType = Cookies.get('diff_view');
const HEAD_HEADER_TEXT = 'HEAD//our changes';
const ORIGIN_HEADER_TEXT = 'origin//their changes';
const HEAD_BUTTON_TITLE = 'Use ours';
@@ -180,9 +181,7 @@
this.state.diffView = viewType;
this.state.isParallel = viewType === VIEW_TYPES.PARALLEL;
- $.cookie('diff_view', viewType, {
- path: gon.relative_url_root || '/'
- });
+ Cookies.set('diff_view', viewType);
},
getHeadHeaderLine(id) {
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6 b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
index 7fd3749b3e2..222a5dcfc2e 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require vue
//= require ./merge_conflict_store
//= require ./merge_conflict_service
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
index 114a2c5b305..c8de586aa21 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_actions.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6 b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
index b846a90ab2a..88c3a20ce13 100644
--- a/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
+++ b/app/assets/javascripts/merge_conflicts/mixins/line_conflict_utils.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
global.mergeConflicts = global.mergeConflicts || {};
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 02ff5a382e2..d3bd1e846c1 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require jquery.waitforimages */
/*= require task_list */
@@ -96,7 +97,11 @@
return $.ajax({
type: 'PATCH',
url: $('form.js-issuable-update').attr('action'),
- data: patchData
+ data: patchData,
+ success: function(mergeRequest) {
+ document.querySelector('#task_status').innerText = mergeRequest.task_status;
+ document.querySelector('#task_status_short').innerText = mergeRequest.task_status_short;
+ }
});
// TODO (rspeicher): Make the merge request description inline-editable like a
// note so that we can re-use its form here
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 3dde979185b..860ee5df57e 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -1,9 +1,10 @@
+/* eslint-disable */
// MergeRequestTabs
//
// Handles persisting and restoring the current tab selection and lazily-loading
// content on the MergeRequests#show page.
//
-/*= require jquery.cookie */
+/*= require js.cookie */
//
// ### Example Markup
@@ -237,8 +238,11 @@
_this.expandViewContainer();
}
_this.diffsLoaded = true;
- _this.scrollToElement("#diffs");
- _this.highlighSelectedLine();
+ var anchoredDiff = gl.utils.getLocationHash();
+ if (anchoredDiff) _this.openAnchoredDiff(anchoredDiff, function() {
+ _this.scrollToElement("#diffs");
+ _this.highlighSelectedLine();
+ });
_this.filesCommentButton = $('.files .diff-file').filesCommentButton();
return $(document).off('click', '.diff-line-num a').on('click', '.diff-line-num a', function(e) {
e.preventDefault();
@@ -251,6 +255,17 @@
});
};
+ MergeRequestTabs.prototype.openAnchoredDiff = function(anchoredDiff, cb) {
+ var diffTitle = $('#file-path-' + anchoredDiff);
+ var diffFile = diffTitle.closest('.diff-file');
+ var nothingHereBlock = $('.nothing-here-block:visible', diffFile);
+ if (nothingHereBlock.length) {
+ diffFile.singleFileDiff(true, cb);
+ } else {
+ cb();
+ }
+ };
+
MergeRequestTabs.prototype.highlighSelectedLine = function() {
var $diffLine, diffLineTop, hashClassString, locationHash, navBarHeight;
$('.hll').removeClass('hll');
@@ -368,7 +383,7 @@
MergeRequestTabs.prototype.expandView = function() {
var $gutterIcon;
- if ($.cookie('collapsed_gutter') === 'true') {
+ if (Cookies.get('collapsed_gutter') === 'true') {
return;
}
$gutterIcon = $('.js-sidebar-toggle i:visible');
diff --git a/app/assets/javascripts/merge_request_widget.js.es6 b/app/assets/javascripts/merge_request_widget.js.es6
index 3ff6851d59b..3a2fe454b68 100644
--- a/app/assets/javascripts/merge_request_widget.js.es6
+++ b/app/assets/javascripts/merge_request_widget.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
var 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; };
diff --git a/app/assets/javascripts/merged_buttons.js b/app/assets/javascripts/merged_buttons.js
index 1fed38661a2..7ad86d8c084 100644
--- a/app/assets/javascripts/merged_buttons.js
+++ b/app/assets/javascripts/merged_buttons.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index bc1a99057d9..9299c96e8ea 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Milestone = (function() {
Milestone.updateIssue = function(li, issue_url, data) {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index cee42633c79..c909b53dc21 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.MilestoneSelect = (function() {
function MilestoneSelect(currentProject) {
@@ -101,6 +102,7 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(selected, $el, e) {
var data, isIssueIndex, isMRIndex, page;
page = $('body').data('page');
@@ -110,7 +112,7 @@
e.preventDefault();
return;
}
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = selected.name;
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
@@ -123,6 +125,24 @@
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if (selected.id !== -1) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'milestone', new ListMilestone({
+ id: selected.id,
+ title: selected.name
+ }));
+ } else {
+ Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'milestone');
+ }
+
+ $dropdown.trigger('loading.gl.dropdown');
+ $loading.fadeIn();
+
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $dropdown.trigger('loaded.gl.dropdown');
+ $loading.fadeOut();
+ });
} else {
selected = $selectbox.find('input[type="hidden"]').val();
data = {};
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index 10f4fd106d8..d1168227b77 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/network/branch_graph.js b/app/assets/javascripts/network/branch_graph.js
index 91132af273a..74dbeb94741 100644
--- a/app/assets/javascripts/network/branch_graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/network/network.js
index 7baebcd100a..8898e7ace43 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/network/network.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Network = (function() {
function Network(opts) {
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
index 67c3e645364..42d6799c82f 100644
--- a/app/assets/javascripts/network/network_bundle.js
+++ b/app/assets/javascripts/network/network_bundle.js
@@ -1,5 +1,6 @@
+/* eslint-disable */
// 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
+// Add new JavaScript 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.
diff --git a/app/assets/javascripts/new_branch_form.js b/app/assets/javascripts/new_branch_form.js
index 20aa2fced27..0e643b0ff14 100644
--- a/app/assets/javascripts/new_branch_form.js
+++ b/app/assets/javascripts/new_branch_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
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; };
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 21bf8867f7b..acb529023fa 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 866a04d3e21..4976eef2896 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require autosave */
/*= require autosize */
diff --git a/app/assets/javascripts/notifications_dropdown.js b/app/assets/javascripts/notifications_dropdown.js
index a41e9d3fabe..ef3f2c6ae73 100644
--- a/app/assets/javascripts/notifications_dropdown.js
+++ b/app/assets/javascripts/notifications_dropdown.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.NotificationsDropdown = (function() {
function NotificationsDropdown() {
diff --git a/app/assets/javascripts/notifications_form.js b/app/assets/javascripts/notifications_form.js
index 6b2ef17ef6b..6fbec8efe9b 100644
--- a/app/assets/javascripts/notifications_form.js
+++ b/app/assets/javascripts/notifications_form.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/pager.js b/app/assets/javascripts/pager.js
index b81ed50cb48..2e4dc62273e 100644
--- a/app/assets/javascripts/pager.js
+++ b/app/assets/javascripts/pager.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Pager = {
init: function(limit, preload, disable, callback) {
diff --git a/app/assets/javascripts/pipelines.js.es6 b/app/assets/javascripts/pipelines.js.es6
index a7624de6089..e6fada5c84c 100644
--- a/app/assets/javascripts/pipelines.js.es6
+++ b/app/assets/javascripts/pipelines.js.es6
@@ -1,37 +1,41 @@
+/* eslint-disable */
((global) => {
class Pipelines {
constructor() {
- $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph);
+ this.initGraphToggle();
this.addMarginToBuildColumns();
}
- toggleGraph() {
- const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
- const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
- const $btnText = $(this).find('.toggle-btn-text');
- const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
-
- $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
-
+ initGraphToggle() {
+ this.pipelineGraph = document.querySelector('.pipeline-graph');
+ this.toggleButton = document.querySelector('.toggle-pipeline-btn');
+ this.toggleButtonText = this.toggleButton.querySelector('.toggle-btn-text');
+ this.toggleButton.addEventListener('click', this.toggleGraph.bind(this));
+ }
- graphCollapsed ? $btnText.text('Hide') : $btnText.text('Expand')
+ toggleGraph() {
+ const graphCollapsed = this.pipelineGraph.classList.contains('graph-collapsed');
+ this.toggleButton.classList.toggle('graph-collapsed');
+ this.pipelineGraph.classList.toggle('graph-collapsed');
+ this.toggleButtonText.textContent = graphCollapsed ? 'Hide' : 'Expand';
}
addMarginToBuildColumns() {
- const $secondChildBuildNode = $('.build:nth-child(2)');
- if ($secondChildBuildNode.length) {
- const $firstChildBuildNode = $secondChildBuildNode.prev('.build');
- const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column');
- const $previousColumn = $multiBuildColumn.prev('.stage-column');
- $multiBuildColumn.addClass('left-margin');
- $firstChildBuildNode.addClass('left-connector');
- $previousColumn.each(function() {
- $this = $(this);
- if ($('.build', $this).length === 1) $this.addClass('no-margin');
- });
+ const secondChildBuildNodes = this.pipelineGraph.querySelectorAll('.build:nth-child(2)');
+ for (buildNodeIndex in secondChildBuildNodes) {
+ const buildNode = secondChildBuildNodes[buildNodeIndex];
+ const firstChildBuildNode = buildNode.previousElementSibling;
+ if (!firstChildBuildNode || !firstChildBuildNode.matches('.build')) continue;
+ const multiBuildColumn = buildNode.closest('.stage-column');
+ const previousColumn = multiBuildColumn.previousElementSibling;
+ if (!previousColumn || !previousColumn.matches('.stage-column')) continue;
+ multiBuildColumn.classList.add('left-margin');
+ firstChildBuildNode.classList.add('left-connector');
+ const columnBuilds = previousColumn.querySelectorAll('.build');
+ if (columnBuilds.length === 1) previousColumn.classList.add('no-margin');
}
- $('.pipeline-graph').removeClass('hidden');
+ this.pipelineGraph.classList.remove('hidden');
}
}
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 5200487814f..f2a45a18bed 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// MarkdownPreview
//
// Handles toggling the "Write" and "Preview" tab clicks, rendering the preview,
diff --git a/app/assets/javascripts/profile/gl_crop.js.es6 b/app/assets/javascripts/profile/gl_crop.js.es6
index a1b0126e857..6da6c1d0295 100644
--- a/app/assets/javascripts/profile/gl_crop.js.es6
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
// Matches everything but the file name
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
index b2307be73ad..73858388261 100644
--- a/app/assets/javascripts/profile/profile.js.es6
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
class Profile {
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
index d6e4d9f7ad8..22bee0f6187 100644
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ b/app/assets/javascripts/profile/profile_bundle.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require_tree . */
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index a6c015299a0..2d0c6b16699 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Project = (function() {
function Project() {
@@ -23,16 +24,12 @@
return $(this).parents('form').submit();
});
$('.hide-no-ssh-message').on('click', function(e) {
- $.cookie('hide_no_ssh_message', 'false', {
- path: gon.relative_url_root || '/'
- });
+ Cookies.set('hide_no_ssh_message', 'false');
$(this).parents('.no-ssh-key-message').remove();
return e.preventDefault();
});
$('.hide-no-password-message').on('click', function(e) {
- $.cookie('hide_no_password_message', 'false', {
- path: gon.relative_url_root || '/'
- });
+ Cookies.set('hide_no_password_message', 'false');
$(this).parents('.no-password-message').remove();
return e.preventDefault();
});
@@ -82,7 +79,7 @@
if (ref.header != null) {
return $('<li />').addClass('dropdown-header').text(ref.header);
} else {
- link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', escape(ref));
+ link = $('<a />').attr('href', '#').addClass(ref === selected ? 'is-active' : '').text(ref).attr('data-ref', ref);
return $('<li />').append(link);
}
},
diff --git a/app/assets/javascripts/project_avatar.js b/app/assets/javascripts/project_avatar.js
index 277e71523d5..61877c6616d 100644
--- a/app/assets/javascripts/project_avatar.js
+++ b/app/assets/javascripts/project_avatar.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ProjectAvatar = (function() {
function ProjectAvatar() {
diff --git a/app/assets/javascripts/project_find_file.js b/app/assets/javascripts/project_find_file.js
index b8347367717..ddac5ed83e1 100644
--- a/app/assets/javascripts/project_find_file.js
+++ b/app/assets/javascripts/project_find_file.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/project_fork.js b/app/assets/javascripts/project_fork.js
index d2261c51f35..fd95f8f2c19 100644
--- a/app/assets/javascripts/project_fork.js
+++ b/app/assets/javascripts/project_fork.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ProjectFork = (function() {
function ProjectFork() {
diff --git a/app/assets/javascripts/project_import.js b/app/assets/javascripts/project_import.js
index c61b0cf2fde..f1c4a9fe542 100644
--- a/app/assets/javascripts/project_import.js
+++ b/app/assets/javascripts/project_import.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ProjectImport = (function() {
function ProjectImport() {
diff --git a/app/assets/javascripts/project_new.js b/app/assets/javascripts/project_new.js
index 478e82aa14d..40575caa57f 100644
--- a/app/assets/javascripts/project_new.js
+++ b/app/assets/javascripts/project_new.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 4239ed2f889..b74b4ae68ff 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ProjectSelect = (function() {
function ProjectSelect() {
diff --git a/app/assets/javascripts/project_show.js b/app/assets/javascripts/project_show.js
index c8cfc9a9ba8..21650f5f67a 100644
--- a/app/assets/javascripts/project_show.js
+++ b/app/assets/javascripts/project_show.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ProjectShow = (function() {
function ProjectShow() {}
diff --git a/app/assets/javascripts/projects_list.js b/app/assets/javascripts/projects_list.js
index 04fb49552e8..3458cd89ae2 100644
--- a/app/assets/javascripts/projects_list.js
+++ b/app/assets/javascripts/projects_list.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.ProjectsList = {
init: function() {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
index 7aeb5f92514..2d60947a666 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
index 46beca469b9..c45c9d8ff22 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
index 983322cbecc..e3f226e9a2a 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_dropdown.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
class ProtectedBranchDropdown {
constructor(options) {
this.onSelect = options.onSelect;
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
index 15a6dca2875..ac3142ffb07 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6 b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
index 9ff0fd12c76..705378a364d 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit_list.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/protected_branches/protected_branches_bundle.js b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
index 15b3affd469..17e34163831 100644
--- a/app/assets/javascripts/protected_branches/protected_branches_bundle.js
+++ b/app/assets/javascripts/protected_branches/protected_branches_bundle.js
@@ -1 +1,2 @@
+/* eslint-disable */
/*= require_tree . */
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index e3d5f413c77..df38937858f 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -5,15 +6,24 @@
function Sidebar(currentUser) {
this.toggleTodo = bind(this.toggleTodo, this);
this.sidebar = $('aside');
+ this.removeListeners();
this.addEventListeners();
}
+ Sidebar.prototype.removeListeners = function () {
+ this.sidebar.off('click', '.sidebar-collapsed-icon');
+ $('.dropdown').off('hidden.gl.dropdown');
+ $('.dropdown').off('loading.gl.dropdown');
+ $('.dropdown').off('loaded.gl.dropdown');
+ $(document).off('click', '.js-sidebar-toggle');
+ }
+
Sidebar.prototype.addEventListeners = function() {
this.sidebar.on('click', '.sidebar-collapsed-icon', this, this.sidebarCollapseClicked);
$('.dropdown').on('hidden.gl.dropdown', this, this.onSidebarDropdownHidden);
$('.dropdown').on('loading.gl.dropdown', this.sidebarDropdownLoading);
$('.dropdown').on('loaded.gl.dropdown', this.sidebarDropdownLoaded);
- $(document).off('click', '.js-sidebar-toggle').on('click', '.js-sidebar-toggle', function(e, triggered) {
+ $(document).on('click', '.js-sidebar-toggle', function(e, triggered) {
var $allGutterToggleIcons, $this, $thisIcon;
e.preventDefault();
$this = $(this);
@@ -29,9 +39,7 @@
$('.page-with-sidebar').removeClass('right-sidebar-collapsed').addClass('right-sidebar-expanded');
}
if (!triggered) {
- return $.cookie("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'), {
- path: gon.relative_url_root || '/'
- });
+ return Cookies.set("collapsed_gutter", $('.right-sidebar').hasClass('right-sidebar-collapsed'));
}
});
return $(document).off('click', '.js-issuable-todo').on('click', '.js-issuable-todo', this.toggleTodo);
@@ -74,16 +82,11 @@
};
Sidebar.prototype.todoUpdateDone = function(data, $btn, $btnText, $todoLoading) {
- var $todoPendingCount;
- $todoPendingCount = $('.todos-pending-count');
- $todoPendingCount.text(data.count);
+ $(document).trigger('todo:toggle', data.count);
+
$btn.enable();
$todoLoading.addClass('hidden');
- if (data.count === 0) {
- $todoPendingCount.addClass('hidden');
- } else {
- $todoPendingCount.removeClass('hidden');
- }
+
if (data.delete_path != null) {
$btn.attr('aria-label', $btn.data('mark-text')).attr('data-delete-path', data.delete_path);
return $btnText.text($btn.data('mark-text'));
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index 8074a94f33e..6c2389f202f 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Search = (function() {
function Search() {
diff --git a/app/assets/javascripts/search_autocomplete.js.es6 b/app/assets/javascripts/search_autocomplete.js.es6
index b4c6226dc68..5fa94556501 100644
--- a/app/assets/javascripts/search_autocomplete.js.es6
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
const KEYCODE = {
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 3aa8536d40a..8d8ab6dda5e 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/shortcuts_blob.js b/app/assets/javascripts/shortcuts_blob.js
index b931eab638f..704a8bd3a57 100644
--- a/app/assets/javascripts/shortcuts_blob.js
+++ b/app/assets/javascripts/shortcuts_blob.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_dashboard_navigation.js b/app/assets/javascripts/shortcuts_dashboard_navigation.js
index f7492a2aa5c..befe4eccdba 100644
--- a/app/assets/javascripts/shortcuts_dashboard_navigation.js
+++ b/app/assets/javascripts/shortcuts_dashboard_navigation.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_find_file.js b/app/assets/javascripts/shortcuts_find_file.js
index 92ce31969e3..90ed4267661 100644
--- a/app/assets/javascripts/shortcuts_find_file.js
+++ b/app/assets/javascripts/shortcuts_find_file.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 235bf4f95ec..25ec7dbc067 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require mousetrap */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/shortcuts_navigation.js b/app/assets/javascripts/shortcuts_navigation.js
index b04159420d1..19c6b7d30ab 100644
--- a/app/assets/javascripts/shortcuts_navigation.js
+++ b/app/assets/javascripts/shortcuts_navigation.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require shortcuts */
diff --git a/app/assets/javascripts/shortcuts_network.js b/app/assets/javascripts/shortcuts_network.js
index fb2b39e757e..002e979a2c6 100644
--- a/app/assets/javascripts/shortcuts_network.js
+++ b/app/assets/javascripts/shortcuts_network.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require shortcuts_navigation */
diff --git a/app/assets/javascripts/sidebar.js.es6 b/app/assets/javascripts/sidebar.js.es6
index 755fac8107b..a23ca449c4b 100644
--- a/app/assets/javascripts/sidebar.js.es6
+++ b/app/assets/javascripts/sidebar.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
let singleton;
@@ -28,7 +29,7 @@
}
init() {
- this.isPinned = $.cookie(pinnedStateCookie) === 'true';
+ this.isPinned = Cookies.get(pinnedStateCookie) === 'true';
this.isExpanded = (
window.innerWidth >= sidebarBreakpoint &&
$(pageSelector).hasClass(expandedPageClass)
@@ -37,7 +38,8 @@
.on('click', sidebarToggleSelector, () => this.toggleSidebar())
.on('click', pinnedToggleSelector, () => this.togglePinnedState())
.on('click', 'html, body', (e) => this.handleClickEvent(e))
- .on('page:change', () => this.renderState());
+ .on('page:change', () => this.renderState())
+ .on('todo:toggle', (e, count) => this.updateTodoCount(count));
this.renderState();
}
@@ -52,6 +54,10 @@
}
}
+ updateTodoCount(count) {
+ $('.js-todos-count').text(gl.text.addDelimiter(count));
+ }
+
toggleSidebar() {
this.isExpanded = !this.isExpanded;
this.renderState();
@@ -62,10 +68,7 @@
if (!this.isPinned) {
this.isExpanded = false;
}
- $.cookie(pinnedStateCookie, this.isPinned ? 'true' : 'false', {
- path: gon.relative_url_root || '/',
- expires: 3650
- });
+ Cookies.set(pinnedStateCookie, this.isPinned ? 'true' : 'false', { expires: 3650 });
this.renderState();
}
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index ee6af123268..8e54ca4f0dc 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -12,7 +13,7 @@
COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
- function SingleFileDiff(file) {
+ function SingleFileDiff(file, forceLoad, cb) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
@@ -31,9 +32,12 @@
this.$toggleIcon.addClass('fa-caret-down');
}
$('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
+ if (forceLoad) {
+ this.toggleDiff(null, cb);
+ }
}
- SingleFileDiff.prototype.toggleDiff = function(e) {
+ SingleFileDiff.prototype.toggleDiff = function(e, cb) {
var $target = $(e.target);
if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
@@ -53,11 +57,11 @@
}
} else {
this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
- return this.getContentHTML();
+ return this.getContentHTML(cb);
}
};
- SingleFileDiff.prototype.getContentHTML = function() {
+ SingleFileDiff.prototype.getContentHTML = function(cb) {
this.collapsedContent.hide();
this.loadingContent.show();
$.get(this.diffForPath, (function(_this) {
@@ -75,6 +79,8 @@
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
}
+
+ if (cb) cb();
};
})(this));
};
@@ -83,10 +89,10 @@
})();
- $.fn.singleFileDiff = function() {
+ $.fn.singleFileDiff = function(forceLoad, cb) {
return this.each(function() {
- if (!$.data(this, 'singleFileDiff')) {
- return $.data(this, 'singleFileDiff', new SingleFileDiff(this));
+ if (!$.data(this, 'singleFileDiff') || forceLoad) {
+ return $.data(this, 'singleFileDiff', new SingleFileDiff(this, forceLoad, cb));
}
});
};
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index 855e97eb301..083dc23c796 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require_tree . */
(function() {
diff --git a/app/assets/javascripts/snippets_list.js.es6 b/app/assets/javascripts/snippets_list.js.es6
index 6f0996c0d2a..c3afc3f2246 100644
--- a/app/assets/javascripts/snippets_list.js.es6
+++ b/app/assets/javascripts/snippets_list.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
(global => {
global.gl = global.gl || {};
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 10509313c12..cfd1e2204d5 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.Star = (function() {
function Star() {
@@ -10,11 +11,9 @@
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
$starSpan.removeClass('starred').text('Star');
- gl.utils.updateTooltipTitle($this, 'Star project');
$starIcon.removeClass('fa-star').addClass('fa-star-o');
} else {
$starSpan.addClass('starred').text('Unstar');
- gl.utils.updateTooltipTitle($this, 'Unstar project');
$starIcon.removeClass('fa-star-o').addClass('fa-star');
}
};
diff --git a/app/assets/javascripts/subscription.js b/app/assets/javascripts/subscription.js
index 5e3c5983d75..f9915593657 100644
--- a/app/assets/javascripts/subscription.js
+++ b/app/assets/javascripts/subscription.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
@@ -5,10 +6,10 @@
function Subscription(container) {
this.toggleSubscription = bind(this.toggleSubscription, this);
var $container;
- $container = $(container);
- this.url = $container.attr('data-url');
- this.subscribe_button = $container.find('.js-subscribe-button');
- this.subscription_status = $container.find('.subscription-status');
+ this.$container = $(container);
+ this.url = this.$container.attr('data-url');
+ this.subscribe_button = this.$container.find('.js-subscribe-button');
+ this.subscription_status = this.$container.find('.subscription-status');
this.subscribe_button.unbind('click').click(this.toggleSubscription);
}
@@ -18,17 +19,27 @@
action = btn.find('span').text();
current_status = this.subscription_status.attr('data-status');
btn.addClass('disabled');
+
+ if ($('html').hasClass('issue-boards-page')) {
+ this.url = this.$container.attr('data-url');
+ }
+
return $.post(this.url, (function(_this) {
return function() {
var status;
btn.removeClass('disabled');
- status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
- _this.subscription_status.attr('data-status', status);
- action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
- btn.find('span').text(action);
- _this.subscription_status.find('>div').toggleClass('hidden');
- if (btn.attr('data-original-title')) {
- return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
+
+ if ($('html').hasClass('issue-boards-page')) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'subscribed', !gl.issueBoards.BoardsStore.detail.issue.subscribed);
+ } else {
+ status = current_status === 'subscribed' ? 'unsubscribed' : 'subscribed';
+ _this.subscription_status.attr('data-status', status);
+ action = status === 'subscribed' ? 'Unsubscribe' : 'Subscribe';
+ btn.find('span').text(action);
+ _this.subscription_status.find('>div').toggleClass('hidden');
+ if (btn.attr('data-original-title')) {
+ return btn.tooltip('hide').attr('data-original-title', action).tooltip('fixTitle');
+ }
}
};
})(this));
diff --git a/app/assets/javascripts/subscription_select.js b/app/assets/javascripts/subscription_select.js
index d6c219603d1..2ca65cb762d 100644
--- a/app/assets/javascripts/subscription_select.js
+++ b/app/assets/javascripts/subscription_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.SubscriptionSelect = (function() {
function SubscriptionSelect() {
diff --git a/app/assets/javascripts/syntax_highlight.js b/app/assets/javascripts/syntax_highlight.js
index 2ae7bf5fc15..77ad4f30b7a 100644
--- a/app/assets/javascripts/syntax_highlight.js
+++ b/app/assets/javascripts/syntax_highlight.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Syntax Highlighter
//
// Applies a syntax highlighting color scheme CSS class to any element with the
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index bd4e3c3d00d..93a3d67ee9f 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require ../blob/template_selector */
((global) => {
@@ -32,24 +33,22 @@
this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner();
- this.setInputValueToTemplateContent(true);
+ this.setInputValueToTemplateContent();
});
return;
}
- setInputValueToTemplateContent(append) {
+ setInputValueToTemplateContent() {
// `this.requestFileSuccess` sets the value of the description input field
- // to the content of the template selected. If `append` is true, the
- // template content will be appended to the previous value of the field,
- // separated by a blank line if the previous value is non-empty.
+ // to the content of the template selected.
if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and
// skip focusing the description input by setting `true` as the
// `skipFocus` option to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
+ this.requestFileSuccess(this.currentTemplate, {skipFocus: true});
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
+ this.requestFileSuccess(this.currentTemplate, {skipFocus: false});
}
return;
}
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index 4e8247b89e1..0a3890e85fe 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
class IssuableTemplateSelectors {
constructor({ $dropdowns, editor } = {}) {
diff --git a/app/assets/javascripts/todos.js.es6 b/app/assets/javascripts/todos.js.es6
index 055228c5df8..213e80825b7 100644
--- a/app/assets/javascripts/todos.js.es6
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
class Todos {
@@ -97,7 +98,8 @@
}
updateBadges(data) {
- $('.todos-pending .badge, .todos-pending-count').text(data.count);
+ $(document).trigger('todo:toggle', data.count);
+ $('.todos-pending .badge').text(data.count);
return $('.todos-done .badge').text(data.done_count);
}
diff --git a/app/assets/javascripts/tree.js b/app/assets/javascripts/tree.js
index 9b7be17c4fe..70aff4b9a2f 100644
--- a/app/assets/javascripts/tree.js
+++ b/app/assets/javascripts/tree.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.TreeView = (function() {
function TreeView() {
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index ce2930c7fc7..35f2b1e2b25 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> authenticated -> POST to server
diff --git a/app/assets/javascripts/u2f/error.js b/app/assets/javascripts/u2f/error.js
index bc48c67c4f2..aff605169e4 100644
--- a/app/assets/javascripts/u2f/error.js
+++ b/app/assets/javascripts/u2f/error.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index 926912fa988..22fbf9f3a91 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Register U2F (universal 2nd factor) devices for users to authenticate with.
//
// State Flow #1: setup -> in_progress -> registered -> POST to server
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 907e640161a..2eab2d5ae23 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
this.U2FUtil = (function() {
function U2FUtil() {}
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 0f97924d94e..5e869e99fdb 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
global.User = class {
constructor({ action }) {
@@ -23,10 +24,7 @@
hideProjectLimitMessage() {
$('.hide-project-limit-message').on('click', e => {
e.preventDefault();
- const path = gon.relative_url_root || '/';
- $.cookie('hide_project_limit_message', 'false', {
- path: path
- });
+ Cookies.set('hide_project_limit_message', 'false');
$(this).parents('.project-limit-message').remove();
});
}
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
index dfdfa1e7f75..2b310da319c 100644
--- a/app/assets/javascripts/user_tabs.js.es6
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*
UserTabs
diff --git a/app/assets/javascripts/username_validator.js.es6 b/app/assets/javascripts/username_validator.js.es6
index bf4b2e320cd..c4dde575c6e 100644
--- a/app/assets/javascripts/username_validator.js.es6
+++ b/app/assets/javascripts/username_validator.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
((global) => {
const debounceTimeoutDuration = 1000;
const invalidInputClass = 'gl-field-error-outline';
diff --git a/app/assets/javascripts/users/calendar.js b/app/assets/javascripts/users/calendar.js
index 3bd4c3c066f..0ec878e7e60 100644
--- a/app/assets/javascripts/users/calendar.js
+++ b/app/assets/javascripts/users/calendar.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/app/assets/javascripts/users/users_bundle.js b/app/assets/javascripts/users/users_bundle.js
index d6e4d9f7ad8..22bee0f6187 100644
--- a/app/assets/javascripts/users/users_bundle.js
+++ b/app/assets/javascripts/users/users_bundle.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require_tree . */
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 3020b7cc239..3847278e80a 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
slice = [].slice;
@@ -9,7 +10,11 @@
this.usersPath = "/autocomplete/users.json";
this.userPath = "/autocomplete/users/:id.json";
if (currentUser != null) {
- this.currentUser = JSON.parse(currentUser);
+ if (typeof currentUser === 'object') {
+ this.currentUser = currentUser;
+ } else {
+ this.currentUser = JSON.parse(currentUser);
+ }
}
$('.js-user-search').each((function(_this) {
return function(i, dropdown) {
@@ -32,9 +37,30 @@
$value = $block.find('.value');
$collapsedSidebar = $block.find('.sidebar-collapsed-user');
$loading = $block.find('.block-loading').fadeOut();
+
+ var updateIssueBoardsIssue = function () {
+ $loading.fadeIn();
+ gl.issueBoards.BoardsStore.detail.issue.update($dropdown.attr('data-issue-update'))
+ .then(function () {
+ $loading.fadeOut();
+ });
+ };
+
$block.on('click', '.js-assign-yourself', function(e) {
e.preventDefault();
- return assignTo(_this.currentUser.id);
+
+ if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+ id: _this.currentUser.id,
+ username: _this.currentUser.username,
+ name: _this.currentUser.name,
+ avatar_url: _this.currentUser.avatar_url
+ }));
+
+ updateIssueBoardsIssue();
+ } else {
+ return assignTo(_this.currentUser.id);
+ }
});
assignTo = function(selected) {
var data;
@@ -150,6 +176,7 @@
// display:block overrides the hide-collapse rule
return $value.css('display', '');
},
+ vue: $dropdown.hasClass('js-issue-board-sidebar'),
clicked: function(user, $el, e) {
var isIssueIndex, isMRIndex, page, selected;
page = $('body').data('page');
@@ -160,7 +187,7 @@
selectedId = user.id;
return;
}
- if ($('html').hasClass('issue-boards-page')) {
+ if ($('html').hasClass('issue-boards-page') && !$dropdown.hasClass('js-issue-board-sidebar')) {
selectedId = user.id;
gl.issueBoards.BoardsStore.state.filters[$dropdown.data('field-name')] = user.id;
gl.issueBoards.BoardsStore.updateFiltersUrl();
@@ -170,6 +197,19 @@
return Issuable.filterResults($dropdown.closest('form'));
} else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
+ } else if ($dropdown.hasClass('js-issue-board-sidebar')) {
+ if (user.id) {
+ Vue.set(gl.issueBoards.BoardsStore.detail.issue, 'assignee', new ListUser({
+ id: user.id,
+ username: user.username,
+ name: user.name,
+ avatar_url: user.avatar_url
+ }));
+ } else {
+ Vue.delete(gl.issueBoards.BoardsStore.detail.issue, 'assignee');
+ }
+
+ updateIssueBoardsIssue();
} else {
selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").val();
return assignTo(selected);
diff --git a/app/assets/javascripts/wikis.js b/app/assets/javascripts/wikis.js
index 35401231fbf..ad9b842db3c 100644
--- a/app/assets/javascripts/wikis.js
+++ b/app/assets/javascripts/wikis.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require latinise */
diff --git a/app/assets/javascripts/zen_mode.js b/app/assets/javascripts/zen_mode.js
index 777b32b41c9..fa124e7052d 100644
--- a/app/assets/javascripts/zen_mode.js
+++ b/app/assets/javascripts/zen_mode.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// Zen Mode (full screen) textarea
//
/*= provides zen_mode:enter */
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 98e301d3799..ce117c3fba5 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -1,11 +1,36 @@
-.avatar {
+@mixin avatar-size($size, $margin-right) {
+ width: $size;
+ height: $size;
+ margin-right: $margin-right;
+}
+
+.avatar-container {
float: left;
- margin-right: 12px;
+ margin-right: 15px;
+ border-radius: $avatar_radius;
+ border: 1px solid rgba(0, 0, 0, .1);
+ &.s16 { @include avatar-size(16px, 6px); }
+ &.s20 { @include avatar-size(20px, 7px); }
+ &.s24 { @include avatar-size(24px, 8px); }
+ &.s26 { @include avatar-size(26px, 8px); }
+ &.s32 { @include avatar-size(32px, 10px); }
+ &.s36 { @include avatar-size(36px, 10px); }
+ &.s40 { @include avatar-size(40px, 10px); }
+ &.s46 { @include avatar-size(46px, 15px); }
+ &.s48 { @include avatar-size(48px, 10px); }
+ &.s60 { @include avatar-size(60px, 12px); }
+ &.s70 { @include avatar-size(70px, 14px); }
+ &.s90 { @include avatar-size(90px, 15px); }
+ &.s110 { @include avatar-size(110px, 15px); }
+ &.s140 { @include avatar-size(140px, 15px); }
+ &.s160 { @include avatar-size(160px, 20px); }
+}
+
+.avatar {
+ @extend .avatar-container;
width: 40px;
height: 40px;
padding: 0;
- border-radius: $avatar_radius;
- border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline {
float: none;
@@ -20,22 +45,6 @@
border-radius: 0;
border: none;
}
-
- &.s16 { width: 16px; height: 16px; margin-right: 6px; }
- &.s20 { width: 20px; height: 20px; margin-right: 7px; }
- &.s24 { width: 24px; height: 24px; margin-right: 8px; }
- &.s26 { width: 26px; height: 26px; margin-right: 8px; }
- &.s32 { width: 32px; height: 32px; margin-right: 10px; }
- &.s36 { width: 36px; height: 36px; margin-right: 10px; }
- &.s40 { width: 40px; height: 40px; margin-right: 10px; }
- &.s46 { width: 46px; height: 46px; margin-right: 15px; }
- &.s48 { width: 48px; height: 48px; margin-right: 10px; }
- &.s60 { width: 60px; height: 60px; margin-right: 12px; }
- &.s70 { width: 70px; height: 70px; margin-right: 14px; }
- &.s90 { width: 90px; height: 90px; margin-right: 15px; }
- &.s110 { width: 110px; height: 110px; margin-right: 15px; }
- &.s140 { width: 140px; height: 140px; margin-right: 20px; }
- &.s160 { width: 160px; height: 160px; margin-right: 20px; }
}
.identicon {
@@ -54,3 +63,17 @@
&.s140 { font-size: 72px; line-height: 138px; }
&.s160 { font-size: 96px; line-height: 158px; }
}
+
+.image-container {
+ @extend .avatar-container;
+ overflow: hidden;
+ display: flex;
+
+ .avatar {
+ border-radius: 0;
+ border: none;
+ height: auto;
+ margin: 0;
+ align-self: center;
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index c0e9c8bf829..ed21ad83a1c 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -216,7 +216,7 @@
svg,
.fa {
&:not(:last-child) {
- margin-right: 3px;
+ margin-right: 5px;
}
}
}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 1de246600fd..baa38ab60c8 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -15,6 +15,7 @@
@media (max-width: $screen-xs-max) {
width: 100%;
+ min-width: 240px;
}
}
@@ -485,7 +486,7 @@
font-size: 20px;
text-indent: 0;
- &:before {
+ &::before {
display: block;
position: relative;
top: -2px;
@@ -517,7 +518,7 @@
background-color: transparent;
border: 0;
- .ui-icon:before {
+ .ui-icon::before {
color: $md-link-color;
}
}
@@ -526,7 +527,7 @@
.ui-datepicker-prev {
left: 0;
- .ui-icon:before {
+ .ui-icon::before {
content: '\f104';
text-align: left;
}
@@ -535,7 +536,7 @@
.ui-datepicker-next {
right: 0;
- .ui-icon:before {
+ .ui-icon::before {
content: '\f105';
text-align: right;
}
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
index 3f877d86a26..91ab1503439 100644
--- a/app/assets/stylesheets/framework/gitlab-theme.scss
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -21,57 +21,66 @@
background: $color-darker;
}
- .nav-sidebar li {
- a {
- color: $color-light;
-
- &:hover,
- &:focus,
- &:active {
- background: $color-dark;
- }
+ .sidebar-header,
+ .sidebar-action-buttons {
+ color: $color-light;
+ background-color: lighten($color-darker, 5%);
+ }
- i {
+ .nav-sidebar {
+ li {
+ a {
color: $color-light;
- }
-
- path,
- polygon {
- fill: $color-light;
- }
- .count {
- color: $color-light;
- background: $color-dark;
+ &:hover,
+ &:focus,
+ &:active {
+ background: $color-dark;
+ }
+
+ i {
+ color: $color-light;
+ }
+
+ path,
+ polygon {
+ fill: $color-light;
+ }
+
+ .count {
+ color: $color-light;
+ background: $color-dark;
+ }
+
+ svg {
+ position: relative;
+ top: 3px;
+ }
}
- svg {
- position: relative;
- top: 3px;
+ &.separate-item {
+ border-top: 1px solid $color;
}
- }
-
- &.separate-item {
- border-top: 1px solid $color;
- }
- &.active a {
- color: $white-light;
- background: $color-dark;
+ &.active a {
+ color: $white-light;
+ background: $color-dark;
- &.no-highlight {
- border: none;
- }
+ &.no-highlight {
+ border: none;
+ }
- i {
- color: $white-light;
- }
+ i {
+ color: $white-light;
+ }
- path,
- polygon {
- fill: $white-light;
+ path,
+ polygon {
+ fill: $white-light;
+ }
}
}
+
}
}
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 142076f65b2..4993ca7572a 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -49,12 +49,16 @@ header {
font-size: 18px;
padding: 0;
margin: ($header-height - 28) / 2 0;
- margin-left: 10px;
+ margin-left: 8px;
height: 28px;
min-width: 28px;
line-height: 28px;
text-align: center;
+ &.header-user-dropdown-toggle {
+ margin-left: 14px;
+ }
+
&:hover,
&:focus,
&:active {
@@ -227,7 +231,7 @@ header {
float: none !important;
.visible-xs,
- .visable-sm {
+ .visible-sm {
display: table-cell !important;
}
}
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 48e34a0066e..bc0610cc417 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -14,7 +14,7 @@
border-bottom: 1px solid #eee;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
- &:after {
+ &::after {
content: " ";
display: table;
clear: both;
@@ -38,7 +38,7 @@
&.smoke { background-color: $background-color; }
- &:hover {
+ &:not(.ui-sort-disabled):hover {
background: $row-hover;
}
@@ -142,10 +142,6 @@ ul.content-list {
}
}
- .avatar {
- margin-right: 15px;
- }
-
.controls {
float: right;
diff --git a/app/assets/stylesheets/framework/pagination.scss b/app/assets/stylesheets/framework/pagination.scss
index b6f21fd8c91..cb2c351c368 100644
--- a/app/assets/stylesheets/framework/pagination.scss
+++ b/app/assets/stylesheets/framework/pagination.scss
@@ -7,8 +7,70 @@
.pagination {
padding: 0;
}
+
+ .gap,
+ .gap:hover {
+ background-color: $gray-light;
+ padding: $gl-vert-padding;
+ cursor: default;
+ }
}
.panel > .gl-pagination {
margin: 0;
}
+
+/**
+ * Extra-small screen pagination.
+ */
+@media (max-width: 320px) {
+ .gl-pagination {
+ .first,
+ .last {
+ display: none;
+ }
+
+ .page {
+ display: none;
+
+ &.active {
+ display: inline;
+ }
+ }
+ }
+}
+
+/**
+ * Small screen pagination
+ */
+@media (max-width: $screen-xs) {
+ .gl-pagination {
+ .pagination li a {
+ padding: 6px 10px;
+ }
+
+ .page {
+ display: none;
+
+ &.active {
+ display: inline;
+ }
+ }
+ }
+}
+
+/**
+ * Medium screen pagination
+ */
+@media (min-width: $screen-xs) and (max-width: $screen-md-max) {
+ .gl-pagination {
+ .page {
+ display: none;
+
+ &.active,
+ &.sibling {
+ display: inline;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 1d8e64a0e4b..d74c14ee2a4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -59,6 +59,11 @@
padding: 0 !important;
}
+ .sidebar-header {
+ padding: 11px 22px 12px;
+ font-size: 20px;
+ }
+
li {
&.separate-item {
padding-top: 10px;
@@ -164,6 +169,18 @@
padding-left: $sidebar_width;
}
}
+
+ .merge-request-tabs-holder.affix {
+ @media (min-width: $sidebar-breakpoint) {
+ left: $sidebar_width;
+ }
+ }
+
+ &.right-sidebar-expanded {
+ .line-resolve-all-container {
+ display: none;
+ }
+ }
}
header.header-sidebar-pinned {
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index eb63a9f214b..875cded8b4e 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -45,7 +45,7 @@
@media (max-width: $screen-xs-max) {
.timeline {
- &:before {
+ &::before {
background: none;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 266a8024809..070e42d63d2 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -148,7 +148,7 @@
a[href*="/uploads/"],
a[href*="storage.googleapis.com/google-code-attachments/"] {
- &:before {
+ &::before {
margin-right: 4px;
font: normal normal normal 14px/1 FontAwesome;
@@ -158,13 +158,13 @@
content: "\f0c6";
}
- &:hover:before {
+ &:hover::before {
text-decoration: none;
}
}
a.no-attachment-icon {
- &:before {
+ &::before {
display: none;
}
}
@@ -183,13 +183,13 @@
position: absolute;
text-decoration: none;
- &:after {
+ &::after {
content: image-url('icon_anchor.svg');
visibility: hidden;
}
}
- &:hover > a.anchor:after {
+ &:hover > a.anchor::after {
visibility: visible;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b271f8cf332..be2a7ceefff 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -90,6 +90,8 @@ $table-border-color: #f0f0f0;
$background-color: $gray-light;
$dark-background-color: #f5f5f5;
$table-text-gray: #8f8f8f;
+$widget-expand-item: #e8f2f7;
+$widget-inner-border: #eef0f2;
/*
* Text
diff --git a/app/assets/stylesheets/pages/admin.scss b/app/assets/stylesheets/pages/admin.scss
index 63396a6bb29..6cefafd8fc7 100644
--- a/app/assets/stylesheets/pages/admin.scss
+++ b/app/assets/stylesheets/pages/admin.scss
@@ -80,10 +80,13 @@
display: -webkit-flex;
display: -ms-flexbox;
display: flex;
+ white-space: nowrap;
}
.user-details {
flex: 1 1 auto;
+ overflow: hidden;
+ padding-right: 8px;
}
.user-name {
@@ -91,6 +94,12 @@
font-weight: 600;
}
+ .user-name,
+ .user-email {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
.dropdown {
.btn-block {
margin-bottom: 0;
diff --git a/app/assets/stylesheets/pages/awards.scss b/app/assets/stylesheets/pages/awards.scss
index 9282e0ae03b..486ad16ea26 100644
--- a/app/assets/stylesheets/pages/awards.scss
+++ b/app/assets/stylesheets/pages/awards.scss
@@ -1,7 +1,7 @@
.awards {
.emoji-icon {
- width: 20px;
- height: 20px;
+ width: 19px;
+ height: 19px;
}
}
@@ -94,7 +94,7 @@
.award-control {
margin: 3px 5px 3px 0;
- padding: 6px 5px;
+ padding: 5px 6px;
outline: 0;
&:hover,
@@ -127,7 +127,7 @@
.award-control-icon {
float: left;
margin-right: 5px;
- font-size: 20px;
+ font-size: 19px;
}
.award-control-icon-loading {
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index d8fabbdcebe..ef6833c9845 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -45,6 +45,15 @@
.page-with-sidebar {
padding-bottom: 0;
}
+
+ .issues-filters {
+ position: relative;
+ z-index: 999999;
+ }
+}
+
+.boards-app {
+ position: relative;
}
.boards-app-loading {
@@ -66,6 +75,10 @@
height: 475px; // Needed for PhantomJS
height: calc(100vh - 220px);
min-height: 475px;
+
+ &.is-compact {
+ width: calc(100% - 290px);
+ }
}
}
@@ -184,6 +197,10 @@
margin-bottom: 5px;
}
+ &.is-active {
+ background-color: $row-hover;
+ }
+
.label {
border: 0;
outline: 0;
@@ -212,6 +229,10 @@
margin-right: 5px;
font-size: (14px / $issue-boards-font-size) * 1em;
}
+
+ .avatar {
+ margin-left: 0;
+ }
}
.card-number {
@@ -264,3 +285,48 @@
border-width: 1px 0 1px 1px;
}
}
+
+.issue-boards-sidebar {
+ &.right-sidebar {
+ top: 153px;
+ bottom: 0;
+
+ @media (min-width: $screen-sm-min) {
+ top: 220px;
+ }
+ }
+
+ .issuable-sidebar-header {
+ position: relative;
+ }
+
+ .gutter-toggle {
+ position: absolute;
+ top: 0;
+ bottom: 15px;
+ right: 0;
+ width: 22px;
+ color: $gray-darkest;
+
+ svg {
+ position: absolute;
+ top: 50%;
+ margin-top: (-11px / 2);
+ }
+
+ &:hover {
+ path {
+ fill: $gray-darkest;
+ }
+ }
+ }
+
+ .issuable-header-text {
+ width: 100%;
+ padding-right: 35px;
+
+ > strong {
+ font-weight: 600;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index d6a55fbd464..6300ac9662f 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -52,10 +52,25 @@
.build-header {
position: relative;
- padding-right: 40px;
+ padding: 0;
+ display: flex;
+ min-height: 58px;
+ align-items: center;
- @media (min-width: $screen-sm-min) {
- padding-right: 0;
+ .btn-inverted {
+ @include btn-outline($white-light, $blue-normal, $blue-normal, $blue-light, $white-light, $blue-light);
+ }
+
+ @media (max-width: $screen-sm-max) {
+ padding-right: 40px;
+
+ .btn-inverted {
+ display: none;
+ }
+ }
+
+ .header-content {
+ flex: 1;
}
a {
@@ -137,10 +152,15 @@
.retry-link {
color: $gl-link-color;
+ display: none;
&:hover {
text-decoration: underline;
}
+
+ @media (max-width: $screen-sm-max) {
+ display: block;
+ }
}
.stage-item {
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index 8ecac08137b..8ecf7fcb96d 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -33,10 +33,8 @@
&.commit-info-row-header {
line-height: 34px;
-
- @media (min-width: $screen-sm-min) {
- margin-bottom: 0;
- }
+ padding: 10px 0;
+ margin-bottom: 0;
.commit-options-dropdown-caret {
@media (max-width: $screen-sm) {
@@ -80,6 +78,58 @@
}
}
+.js-details-expand {
+ &:hover {
+ text-decoration: none;
+ }
+}
+
+.commit-info-widget {
+ background: $background-color;
+ color: $gl-gray;
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+
+ .widget-row {
+ padding: $gl-padding;
+
+ &:not(:last-of-type) {
+ border-bottom: 1px solid $widget-inner-border;
+ }
+
+ &.branch-info {
+ .monospace,
+ .commit-info {
+ margin-left: 4px;
+ }
+ }
+ }
+
+ .icon-container {
+ display: inline-block;
+ margin-right: 8px;
+
+ svg {
+ position: relative;
+ top: 2px;
+ height: 16px;
+ width: 16px;
+ }
+
+ &.commit-icon {
+ svg {
+ path {
+ fill: $gl-text-color;
+ }
+ }
+ }
+ }
+
+ .label.label-gray {
+ background-color: $widget-expand-item;
+ }
+}
+
.ci-status-link {
svg {
overflow: visible;
@@ -88,6 +138,7 @@
.commit-box {
border-top: 1px solid $border-color;
+ padding: $gl-padding 0;
.commit-title {
margin: 0;
@@ -138,6 +189,9 @@
}
.commit-action-buttons {
+ position: relative;
+ top: -1px;
+
i {
color: $gl-icon-color;
font-size: 13px;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index ad315cfae62..52d6a39bd59 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -33,21 +33,22 @@
color: $gl-dark-link-color;
}
- .text-expander {
- display: inline-block;
- background: $gray-light;
- color: $gl-placeholder-color;
- padding: 0 5px;
- cursor: pointer;
- border: 1px solid $border-gray-dark;
- border-radius: $border-radius-default;
- margin-left: 5px;
- line-height: 1;
-
- &:hover {
- background-color: darken($gray-light, 10%);
- text-decoration: none;
- }
+}
+
+.text-expander {
+ display: inline-block;
+ background: $gray-light;
+ color: $gl-placeholder-color;
+ padding: 0 5px;
+ cursor: pointer;
+ border: 1px solid $border-gray-dark;
+ border-radius: $border-radius-default;
+ margin-left: 5px;
+ line-height: 1;
+
+ &:hover {
+ background-color: darken($gray-light, 10%);
+ text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/pages/dashboard.scss b/app/assets/stylesheets/pages/dashboard.scss
index 76225ed8d06..016bab104eb 100644
--- a/app/assets/stylesheets/pages/dashboard.scss
+++ b/app/assets/stylesheets/pages/dashboard.scss
@@ -36,10 +36,6 @@
}
}
-.dash-project-avatar {
- float: left;
-}
-
.dash-project-access-icon {
float: left;
margin-right: 5px;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index e0367d1d942..fde138c874d 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -94,14 +94,14 @@
position: relative;
&.old {
- &:before {
+ &::before {
content: '-';
position: absolute;
}
}
&.new {
- &:before {
+ &::before {
content: '+';
position: absolute;
}
@@ -471,7 +471,7 @@
.file-holder {
.diff-line-num:not(.js-unfold-bottom) {
a {
- &:before {
+ &::before {
content: attr(data-linenumber);
}
}
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index ee2a398f031..4375e29c8db 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -3,12 +3,14 @@
}
.dashboard .side .panel .panel-heading .input-group {
+
.form-control {
height: 42px;
}
}
.group-row {
+
.stats {
float: right;
line-height: $list-text-height;
@@ -21,12 +23,14 @@
}
.ldap-group-links {
+
.form-actions {
margin-bottom: $gl-padding;
}
}
.groups-cover-block {
+
.container-fluid {
position: relative;
}
@@ -41,9 +45,14 @@
background-color: $background-color;
}
}
+
+ .group-avatar {
+ border: 0;
+ }
}
.groups-header {
+
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 3d2b024fe5c..a2f5c6c6bd3 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -54,7 +54,6 @@
margin: 0 0 10px;
}
-
.login-footer {
margin-top: 10px;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 70afa568554..f8e31a624ec 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -60,7 +60,7 @@
}
.ci_widget {
- border-bottom: 1px solid #eef0f2;
+ border-bottom: 1px solid $widget-inner-border;
svg {
margin-right: 4px;
@@ -279,6 +279,10 @@
#modal_merge_info .modal-dialog {
width: 600px;
+ .dark {
+ margin-right: 40px;
+ }
+
.btn-clipboard {
@extend .pull-right;
@@ -439,12 +443,12 @@
}
.merge-request-tabs-holder {
- background-color: #fff;
+ background-color: $white-light;
&.affix {
top: 100px;
left: 0;
- z-index: 9;
+ z-index: 10;
transition: right .15s;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index b90c91831f2..526e9ae5cdd 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -105,11 +105,6 @@ ul.notes {
padding: 2px;
margin-top: 10px;
}
-
- .award-control {
- font-size: 13px;
- padding: 2px 5px;
- }
}
.note-header {
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index f88175365c6..a8e8bbcb208 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -474,8 +474,8 @@
}
.arrow {
- &:before,
- &:after {
+ &::before,
+ &::after {
content: '';
display: inline-block;
position: absolute;
@@ -486,14 +486,14 @@
top: 18px;
}
- &:before {
+ &::before {
left: -5px;
margin-top: -6px;
border-width: 7px 5px 7px 0;
border-right-color: $border-color;
}
- &:after {
+ &::after {
left: -4px;
margin-top: -9px;
border-width: 10px 7px 10px 0;
@@ -573,8 +573,7 @@
.build {
// Remove right connecting horizontal line from first build in last stage
&:first-child {
- &::after,
- &::before {
+ &::after {
border: none;
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 3f6fdaebc1d..ede29db1979 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -77,14 +77,14 @@
// Middle dot divider between each element in a list of items.
.middle-dot-divider {
- &:after {
+ &::after {
content: "\00B7"; // Middle Dot
padding: 0 6px;
font-weight: bold;
}
&:last-child {
- &:after {
+ &::after {
content: "";
padding: 0;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index f6355941837..f7d54564530 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -96,8 +96,8 @@
.project-avatar {
float: none;
- margin-left: auto;
- margin-right: auto;
+ margin: 0 auto;
+ border: none;
&.identicon {
border-radius: 50%;
@@ -193,7 +193,7 @@
margin-left: 4px;
.arrow {
- &:before {
+ &::before {
content: '';
display: inline-block;
position: absolute;
@@ -209,7 +209,7 @@
pointer-events: none;
}
- &:after {
+ &::after {
content: '';
position: absolute;
width: 0;
@@ -351,7 +351,7 @@ a.deploy-project-label {
line-height: 36px;
margin: 0;
- > li + li:before {
+ > li + li::before {
padding: 0 3px;
color: #999;
}
@@ -790,7 +790,7 @@ pre.light-well {
top: 7px;
color: $location-icon-color;
- &:before {
+ &::before {
font-family: FontAwesome;
font-weight: normal;
font-style: normal;
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 6d472e8293f..bf688af50e2 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -72,7 +72,7 @@
top: 0;
color: $location-icon-color;
- &:before {
+ &::before {
font-family: FontAwesome;
font-weight: normal;
font-style: normal;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index ea76fe18876..b3aef2fdd32 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -161,3 +161,63 @@
}
}
}
+
+.todos-empty {
+ display: -webkit-flex;
+ display: flex;
+ -webkit-flex-direction: column;
+ flex-direction: column;
+ max-width: 900px;
+ margin-left: auto;
+ margin-right: auto;
+
+ @media (min-width: $screen-sm-min) {
+ -webkit-flex-direction: row;
+ flex-direction: row;
+ padding-top: 80px;
+ }
+}
+
+.todos-empty-content {
+ -webkit-align-self: center;
+ align-self: center;
+ max-width: 480px;
+ margin-right: 20px;
+}
+
+.todos-empty-hero {
+ width: 200px;
+ margin-left: auto;
+ margin-right: auto;
+
+ @media (min-width: $screen-sm-min) {
+ width: 300px;
+ margin-right: 0;
+ -webkit-order: 2;
+ order: 2;
+ }
+}
+
+.todos-all-done {
+ padding-top: 20px;
+
+ @media (min-width: $screen-sm-min) {
+ padding-top: 50px;
+ }
+
+ > svg {
+ display: block;
+ max-width: 300px;
+ margin: 0 auto 20px;
+ }
+
+ p {
+ max-width: 470px;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ a {
+ font-weight: 600;
+ }
+}
diff --git a/app/assets/stylesheets/pages/ui_dev_kit.scss b/app/assets/stylesheets/pages/ui_dev_kit.scss
index 587bd6a1e8a..e73cecc92be 100644
--- a/app/assets/stylesheets/pages/ui_dev_kit.scss
+++ b/app/assets/stylesheets/pages/ui_dev_kit.scss
@@ -5,7 +5,7 @@
}
.example {
- &:before {
+ &::before {
content: "Example";
color: #bbb;
}
diff --git a/app/assets/stylesheets/print.scss b/app/assets/stylesheets/print.scss
index 8239b7e6879..0ff3c3f5472 100644
--- a/app/assets/stylesheets/print.scss
+++ b/app/assets/stylesheets/print.scss
@@ -35,7 +35,7 @@ nav.navbar-collapse.collapse,
.nav,
.btn,
ul.notes-form,
-.merge-request-ci-status .ci-status-link:after,
+.merge-request-ci-status .ci-status-link::after,
.issuable-gutter-toggle,
.gutter-toggle,
.issuable-details .content-block-small,
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index f35f4a8c811..bb912ed10cc 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -3,7 +3,7 @@ class Admin::UsersController < Admin::ApplicationController
def index
@users = User.order_name_asc.filter(params[:filter])
- @users = @users.search(params[:name]) if params[:name].present?
+ @users = @users.search_with_secondary_emails(params[:search_query]) if params[:search_query].present?
@users = @users.sort(@sort = params[:sort])
@users = @users.page(params[:page])
end
diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb
index 4cb3be41064..c33d7eecb9f 100644
--- a/app/controllers/concerns/service_params.rb
+++ b/app/controllers/concerns/service_params.rb
@@ -18,7 +18,7 @@ module ServiceParams
:add_pusher, :send_from_committer_email, :disable_diffs,
:external_wiki_url, :notify, :color,
:server_host, :server_port, :default_irc_uri, :enable_ssl_verification,
- :jira_issue_transition_id]
+ :jira_issue_transition_id, :url, :project_key]
# Parameters to ignore if no value is specified
FILTER_BLANK_PARAMS = [:password]
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index a2b01ff43dc..dc33e1405f2 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -73,10 +73,13 @@ module Projects
def serialize_as_json(resource)
resource.as_json(
labels: true,
- only: [:iid, :title, :confidential],
+ only: [:iid, :title, :confidential, :due_date],
include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] }
- })
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+ milestone: { only: [:id, :title] }
+ },
+ user: current_user
+ )
end
end
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index cb649264146..3f1a1d1c511 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -112,7 +112,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
format.json do
- render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
+ render json: @issue.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 4f855134368..42fd09e9b7e 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -126,7 +126,7 @@ class Projects::LabelsController < Projects::ApplicationController
alias_method :subscribable_resource, :label
def find_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute.includes(:priorities)
+ @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
end
def authorize_admin_labels!
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 2ee53f7ceda..30f1cf4e5be 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -278,7 +278,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_request.target_project, @merge_request])
end
format.json do
- render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } })
+ render json: @merge_request.to_json(include: { milestone: {}, assignee: { methods: :avatar_url }, labels: { methods: :text_color } }, methods: [:task_status, :task_status_short])
end
end
else
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index d08f490de18..699a56ae2f8 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -25,18 +25,15 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- if params[:user_ids].blank?
- return redirect_to(namespace_project_project_members_path(@project.namespace, @project), alert: 'No users or groups specified.')
- end
+ status = Members::CreateService.new(@project, current_user, params).execute
- @project.team.add_users(
- params[:user_ids].split(','),
- params[:access_level],
- expires_at: params[:expires_at],
- current_user: current_user
- )
+ redirect_url = namespace_project_project_members_path(@project.namespace, @project)
- redirect_to namespace_project_project_members_path(@project.namespace, @project), notice: 'Users were successfully added.'
+ if status
+ redirect_to redirect_url, notice: 'Users were successfully added.'
+ else
+ redirect_to redirect_url, alert: 'No users or groups specified.'
+ end
end
def update
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 8fea20cefef..953091492ae 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -23,7 +23,7 @@ class Projects::TagsController < Projects::ApplicationController
return render_404 unless @tag
@release = @project.releases.find_or_initialize_by(tag: @tag.name)
- @commit = @repository.commit(@tag.target)
+ @commit = @repository.commit(@tag.dereferenced_target)
end
def create
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 76b730198d4..bce5e29d8d8 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -30,6 +30,8 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params).execute
if @project.saved?
+ cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
+
redirect_to(
project_path(@project),
notice: "Project '#{@project.name}' was successfully created."
@@ -287,7 +289,8 @@ class ProjectsController < Projects::ApplicationController
render 'projects/empty' if @project.empty_repo?
else
if @project.wiki_enabled?
- @wiki_home = @project.wiki.find_page('home', params[:version_id])
+ @project_wiki = @project.wiki
+ @wiki_home = @project_wiki.find_page('home', params[:version_id])
elsif @project.feature_available?(:issues, current_user)
@issues = issues_collection
@issues = @issues.page(params[:page])
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index e27986ef95b..cc2073081b5 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -126,7 +126,7 @@ class IssuableFinder
@labels =
if labels? && !filter_by_no_label?
- LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute
+ LabelsFinder.new(current_user, project_ids: projects, title: label_names).execute(skip_authorization: true)
else
Label.none
end
@@ -273,7 +273,7 @@ class IssuableFinder
items = items.with_label(label_names, params[:sort])
if projects
- label_ids = LabelsFinder.new(current_user, project_ids: projects).execute.select(:id)
+ label_ids = LabelsFinder.new(current_user, project_ids: projects).execute(skip_authorization: true).select(:id)
items = items.where(labels: { id: label_ids })
end
end
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index 95e62cdb02a..865f093f04a 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -4,9 +4,8 @@ class LabelsFinder < UnionFinder
@params = params
end
- def execute(authorized_only: true)
- @authorized_only = authorized_only
-
+ def execute(skip_authorization: false)
+ @skip_authorization = skip_authorization
items = find_union(label_ids, Label)
items = with_title(items)
sort(items)
@@ -14,7 +13,7 @@ class LabelsFinder < UnionFinder
private
- attr_reader :current_user, :params, :authorized_only
+ attr_reader :current_user, :params, :skip_authorization
def label_ids
label_ids = []
@@ -50,7 +49,7 @@ class LabelsFinder < UnionFinder
end
def projects_ids
- params[:project_ids].presence
+ params[:project_ids]
end
def title
@@ -70,17 +69,17 @@ class LabelsFinder < UnionFinder
end
def find_project
- if authorized_only
- available_projects.find_by(id: project_id)
- else
+ if skip_authorization
Project.find_by(id: project_id)
+ else
+ available_projects.find_by(id: project_id)
end
end
def projects
return @projects if defined?(@projects)
- @projects = authorized_only ? available_projects : Project.all
+ @projects = skip_authorization ? Project.all : available_projects
@projects = @projects.in_namespace(group_id) if group_id
@projects = @projects.where(id: projects_ids) if projects_ids
@projects = @projects.reorder(nil)
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 85e1dc33ee8..dee3c78df47 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -16,13 +16,14 @@ module ButtonHelper
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
css_class = data[:class] || 'btn-clipboard btn-transparent'
+ title = data[:title] || 'Copy to Clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
class: "btn #{css_class}",
data: data,
type: :button,
- title: 'Copy to Clipboard'
+ title: title
end
def http_clone_button(project, placement = 'right', append_link: true)
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index f8ded05c31a..00e64076408 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -39,6 +39,12 @@ module EventsHelper
end
end
+ def event_filter_visible(feature_key)
+ return true unless @project
+
+ @project.feature_available?(feature_key, current_user)
+ end
+
def event_preposition(event)
if event.push? || event.commented? || event.target
"at"
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 03b2db1bc91..ef6cfb235a9 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -71,6 +71,14 @@ module IssuablesHelper
author_output = link_to_member(project, issuable.author, size: 24, mobile_classes: "hidden-xs", tooltip: true)
author_output << link_to_member(project, issuable.author, size: 24, by_username: true, avatar: false, mobile_classes: "hidden-sm hidden-md hidden-lg")
end
+
+ if issuable.tasks?
+ output << "&ensp;".html_safe
+ output << content_tag(:span, issuable.task_status, id: "task_status", class: "hidden-xs")
+ output << content_tag(:span, issuable.task_status_short, id: "task_status_short", class: "hidden-sm hidden-md hidden-lg")
+ end
+
+ output
end
def issuable_todo(issuable)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index d26b4018be6..42c00ec3cd5 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -174,10 +174,14 @@ module ProjectsHelper
nav_tabs << :merge_requests
end
- if can?(current_user, :read_build, project)
+ if can?(current_user, :read_pipeline, project)
nav_tabs << :pipelines
end
+ if can?(current_user, :read_build, project)
+ nav_tabs << :builds
+ end
+
if Gitlab.config.registry.enabled && can?(current_user, :read_container_image, project)
nav_tabs << :container_registry
end
diff --git a/app/helpers/sidekiq_helper.rb b/app/helpers/sidekiq_helper.rb
index d440edc55ba..56749d80bd3 100644
--- a/app/helpers/sidekiq_helper.rb
+++ b/app/helpers/sidekiq_helper.rb
@@ -5,7 +5,7 @@ module SidekiqHelper
(?<mem>[\d\.,]+)\s+
(?<state>[DRSTWXZNLsl\+<]+)\s+
(?<start>.+)\s+
- (?<command>sidekiq.*\])\s+
+ (?<command>sidekiq.*\])\s*
\z/x
def parse_sidekiq_ps(line)
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 826539d5c4d..3fee6c18770 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -30,23 +30,23 @@ module Ci
end
event :run do
- transition any => :running
+ transition any - [:running] => :running
end
event :skip do
- transition any => :skipped
+ transition any - [:skipped] => :skipped
end
event :drop do
- transition any => :failed
+ transition any - [:failed] => :failed
end
event :succeed do
- transition any => :success
+ transition any - [:success] => :success
end
event :cancel do
- transition any => :canceled
+ transition any - [:canceled] => :canceled
end
# IMPORTANT
@@ -271,7 +271,7 @@ module Ci
end
def update_status
- with_lock do
+ Gitlab::OptimisticLocking.retry_lock(self) do
case latest_builds_status
when 'pending' then enqueue
when 'running' then run
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 4cb3a69416e..d159fc6c5c7 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -73,16 +73,16 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :pending, :running] => :canceled
end
- after_transition created: [:pending, :running] do |commit_status|
- commit_status.update_attributes queued_at: Time.now
+ before_transition created: [:pending, :running] do |commit_status|
+ commit_status.queued_at = Time.now
end
- after_transition [:created, :pending] => :running do |commit_status|
- commit_status.update_attributes started_at: Time.now
+ before_transition [:created, :pending] => :running do |commit_status|
+ commit_status.started_at = Time.now
end
- after_transition any => [:success, :failed, :canceled] do |commit_status|
- commit_status.update_attributes finished_at: Time.now
+ before_transition any => [:success, :failed, :canceled] do |commit_status|
+ commit_status.finished_at = Time.now
end
after_transition do |commit_status, transition|
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 17c3b526c97..613444e0d70 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -12,6 +12,7 @@ module Issuable
include Subscribable
include StripAttribute
include Awardable
+ include Taskable
included do
cache_markdown_field :title, pipeline: :single_line
diff --git a/app/models/concerns/project_features_compatibility.rb b/app/models/concerns/project_features_compatibility.rb
index 9216122923e..6d88951c713 100644
--- a/app/models/concerns/project_features_compatibility.rb
+++ b/app/models/concerns/project_features_compatibility.rb
@@ -31,7 +31,7 @@ module ProjectFeaturesCompatibility
def write_feature_attribute(field, value)
build_project_feature unless project_feature
- access_level = value == "true" ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
+ access_level = Gitlab::Utils.to_boolean(value) ? ProjectFeature::ENABLED : ProjectFeature::DISABLED
project_feature.update_attribute(field, access_level)
end
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 12b23f00769..7edb0acd56c 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -38,16 +38,21 @@ module Sortable
private
- def highest_label_priority(target_type:, target_column:, project_column:, excluded_labels: [])
+ def highest_label_priority(target_type_column: nil, target_type: nil, target_column:, project_column:, excluded_labels: [])
query = Label.select(LabelPriority.arel_table[:priority].minimum).
left_join_priorities.
joins(:label_links).
where("label_priorities.project_id = #{project_column}").
- where(label_links: { target_type: target_type }).
where("label_links.target_id = #{target_column}").
reorder(nil)
- query.where.not(title: excluded_labels) if excluded_labels.present?
+ if target_type_column
+ query = query.where("label_links.target_type = #{target_type_column}")
+ else
+ query = query.where(label_links: { target_type: target_type })
+ end
+
+ query = query.where.not(title: excluded_labels) if excluded_labels.present?
query
end
diff --git a/app/models/concerns/taskable.rb b/app/models/concerns/taskable.rb
index a3ac577cf3e..ebc75100a54 100644
--- a/app/models/concerns/taskable.rb
+++ b/app/models/concerns/taskable.rb
@@ -53,10 +53,22 @@ module Taskable
# Return a string that describes the current state of this Taskable's task
# list items, e.g. "12 of 20 tasks completed"
- def task_status
+ def task_status(short: false)
return '' if description.blank?
+ prep, completed = if short
+ ['/', '']
+ else
+ [' of ', ' completed']
+ end
+
sum = tasks.summary
- "#{sum.complete_count} of #{sum.item_count} #{'task'.pluralize(sum.item_count)} completed"
+ "#{sum.complete_count}#{prep}#{sum.item_count} #{'task'.pluralize(sum.item_count)}#{completed}"
+ end
+
+ # Return a short string that describes the current state of this Taskable's
+ # task list items -- for small screens
+ def task_status_short
+ task_status(short: true)
end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 3993b35f96d..43e67069b70 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -1,6 +1,6 @@
class Event < ActiveRecord::Base
include Sortable
- default_scope { where.not(author_id: nil) }
+ default_scope { reorder(nil).where.not(author_id: nil) }
CREATED = 1
UPDATED = 2
diff --git a/app/models/group.rb b/app/models/group.rb
index 552e1154df6..d9e90cd256a 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -68,7 +68,7 @@ class Group < Namespace
end
def web_url
- Gitlab::Routing.url_helpers.group_url(self)
+ Gitlab::Routing.url_helpers.group_canonical_url(self)
end
def human_name
diff --git a/app/models/group_label.rb b/app/models/group_label.rb
index a698b532d19..68841ace2e6 100644
--- a/app/models/group_label.rb
+++ b/app/models/group_label.rb
@@ -5,6 +5,10 @@ class GroupLabel < Label
alias_attribute :subject, :group
+ def subject_foreign_key
+ 'group_id'
+ end
+
def to_reference(source_project = nil, target_project = nil, format: :id)
super(source_project, target_project, format: format)
end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 89158a50353..4f02b02c488 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -5,7 +5,6 @@ class Issue < ActiveRecord::Base
include Issuable
include Referable
include Sortable
- include Taskable
include Spammable
include FasterCacheKeys
@@ -287,10 +286,12 @@ class Issue < ActiveRecord::Base
def as_json(options = {})
super(options).tap do |json|
+ json[:subscribed] = subscribed?(options[:user]) if options.has_key?(:user)
+
if options.has_key?(:labels)
json[:labels] = labels.as_json(
project: project,
- only: [:id, :title, :description, :color],
+ only: [:id, :title, :description, :color, :priority],
methods: [:text_color]
)
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 149fd98ecb3..d9287f2dc29 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -92,16 +92,23 @@ class Label < ActiveRecord::Base
nil
end
- def open_issues_count(user = nil, project = nil)
- issues_count(user, project_id: project.try(:id) || project_id, state: 'opened')
+ def open_issues_count(user = nil)
+ issues_count(user, state: 'opened')
end
- def closed_issues_count(user = nil, project = nil)
- issues_count(user, project_id: project.try(:id) || project_id, state: 'closed')
+ def closed_issues_count(user = nil)
+ issues_count(user, state: 'closed')
end
- def open_merge_requests_count(user = nil, project = nil)
- merge_requests_count(user, project_id: project.try(:id) || project_id, state: 'opened')
+ def open_merge_requests_count(user = nil)
+ params = {
+ subject_foreign_key => subject.id,
+ label_name: title,
+ scope: 'all',
+ state: 'opened'
+ }
+
+ MergeRequestsFinder.new(user, params.with_indifferent_access).execute.count
end
def prioritize!(project, value)
@@ -167,15 +174,8 @@ class Label < ActiveRecord::Base
end
def issues_count(user, params = {})
- IssuesFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
- .execute
- .count
- end
-
- def merge_requests_count(user, params = {})
- MergeRequestsFinder.new(user, params.reverse_merge(label_name: title, scope: 'all'))
- .execute
- .count
+ params.merge!(subject_foreign_key => subject.id, label_name: title, scope: 'all')
+ IssuesFinder.new(user, params.with_indifferent_access).execute.count
end
def label_format_reference(format = :id)
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 18657c3e1c8..7712d5783e0 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -17,4 +17,10 @@ class LfsObject < ActiveRecord::Base
def project_allowed_access?(project)
projects.exists?(storage_project(project).id)
end
+
+ def self.destroy_unreferenced
+ joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
+ .where(lfs_objects_projects: { id: nil })
+ .destroy_all
+ end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 4872f8b8649..0397c57f935 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -3,7 +3,6 @@ class MergeRequest < ActiveRecord::Base
include Issuable
include Referable
include Sortable
- include Taskable
include Importable
belongs_to :target_project, class_name: "Project"
diff --git a/app/models/project.rb b/app/models/project.rb
index fbf7012972e..d5512dfaf9c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -30,6 +30,11 @@ class Project < ActiveRecord::Base
default_value_for :container_registry_enabled, gitlab_config_features.container_registry
default_value_for(:repository_storage) { current_application_settings.repository_storage }
default_value_for(:shared_runners_enabled) { current_application_settings.shared_runners_enabled }
+ default_value_for :issues_enabled, gitlab_config_features.issues
+ default_value_for :merge_requests_enabled, gitlab_config_features.merge_requests
+ default_value_for :builds_enabled, gitlab_config_features.builds
+ default_value_for :wiki_enabled, gitlab_config_features.wiki
+ default_value_for :snippets_enabled, gitlab_config_features.snippets
after_create :ensure_dir_exist
after_create :create_project_feature, unless: :project_feature
@@ -390,7 +395,7 @@ class Project < ActiveRecord::Base
end
def group_ids
- joins(:namespace).where(namespaces: { type: 'Group' }).pluck(:namespace_id)
+ joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
end
end
@@ -738,7 +743,7 @@ class Project < ActiveRecord::Base
def create_labels
Label.templates.each do |label|
params = label.attributes.except('id', 'template', 'created_at', 'updated_at')
- Labels::FindOrCreateService.new(owner, self, params).execute
+ Labels::FindOrCreateService.new(nil, self, params).execute(skip_authorization: true)
end
end
diff --git a/app/models/project_label.rb b/app/models/project_label.rb
index 33c2b617715..82f47f0e8fd 100644
--- a/app/models/project_label.rb
+++ b/app/models/project_label.rb
@@ -12,6 +12,10 @@ class ProjectLabel < Label
alias_attribute :subject, :project
+ def subject_foreign_key
+ 'project_id'
+ end
+
def to_reference(target_project = nil, format: :id)
super(project, target_project, format: format)
end
diff --git a/app/models/project_services/bugzilla_service.rb b/app/models/project_services/bugzilla_service.rb
index 81af55aa29a..338e685339a 100644
--- a/app/models/project_services/bugzilla_service.rb
+++ b/app/models/project_services/bugzilla_service.rb
@@ -1,4 +1,6 @@
class BugzillaService < IssueTrackerService
+ validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def title
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index d9fba3d4a41..b2f426dc2ac 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -1,4 +1,6 @@
class CustomIssueTrackerService < IssueTrackerService
+ validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def title
diff --git a/app/models/project_services/gitlab_issue_tracker_service.rb b/app/models/project_services/gitlab_issue_tracker_service.rb
index 5d17c358330..6bd8d4ec568 100644
--- a/app/models/project_services/gitlab_issue_tracker_service.rb
+++ b/app/models/project_services/gitlab_issue_tracker_service.rb
@@ -1,6 +1,8 @@
class GitlabIssueTrackerService < IssueTrackerService
include Gitlab::Routing.url_helpers
+ validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
default_value_for :default, true
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index b26ddd518d7..207bb816ad1 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -1,6 +1,4 @@
class IssueTrackerService < Service
- validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
-
default_value_for :category, 'issue_tracker'
# Pattern used to extract links from comments
@@ -38,18 +36,24 @@ class IssueTrackerService < Service
]
end
- def initialize_properties
- if properties.nil?
- if enabled_in_gitlab_config
+ # Initialize with default properties values
+ # or receive a block with custom properties
+ def initialize_properties(&block)
+ return unless properties.nil?
+
+ if enabled_in_gitlab_config
+ if block_given?
+ yield
+ else
self.properties = {
title: issues_tracker['title'],
project_url: issues_tracker['project_url'],
issues_url: issues_tracker['issues_url'],
new_issue_url: issues_tracker['new_issue_url']
}
- else
- self.properties = {}
end
+ else
+ self.properties = {}
end
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index f81b66fd219..5bcf199d468 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,15 +1,32 @@
+# == Schema Information
+#
+# Table name: services
+#
+# id :integer not null, primary key
+# type :string(255)
+# title :string(255)
+# project_id :integer
+# created_at :datetime
+# updated_at :datetime
+# active :boolean default(FALSE), not null
+# properties :text
+# template :boolean default(FALSE)
+# push_events :boolean default(TRUE)
+# issues_events :boolean default(TRUE)
+# merge_requests_events :boolean default(TRUE)
+# tag_push_events :boolean default(TRUE)
+# note_events :boolean default(TRUE), not null
+# build_events :boolean default(FALSE), not null
+#
+
class JiraService < IssueTrackerService
- include HTTParty
include Gitlab::Routing.url_helpers
- DEFAULT_API_VERSION = 2
-
- prop_accessor :username, :password, :api_url, :jira_issue_transition_id,
- :title, :description, :project_url, :issues_url, :new_issue_url
+ validates :url, url: true, presence: true, if: :activated?
+ validates :project_key, presence: true, if: :activated?
- validates :api_url, presence: true, url: true, if: :activated?
-
- before_validation :set_api_url, :set_jira_issue_transition_id
+ prop_accessor :username, :password, :url, :project_key,
+ :jira_issue_transition_id, :title, :description
before_update :reset_password
@@ -18,16 +35,46 @@ class JiraService < IssueTrackerService
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
+ def initialize_properties
+ super do
+ self.properties = {
+ title: issues_tracker['title'],
+ url: issues_tracker['url']
+ }
+ end
+ end
+
def reset_password
# don't reset the password if a new one is provided
- if api_url_changed? && !password_touched?
+ if url_changed? && !password_touched?
self.password = nil
end
end
+ def options
+ url = URI.parse(self.url)
+
+ {
+ username: self.username,
+ password: self.password,
+ site: URI.join(url, '/').to_s,
+ context_path: url.path,
+ auth_type: :basic,
+ read_timeout: 120,
+ use_ssl: url.scheme == 'https'
+ }
+ end
+
+ def client
+ @client ||= JIRA::Client.new(options)
+ end
+
+ def jira_project
+ @jira_project ||= client.Project.find(project_key)
+ end
+
def help
- 'Setting `project_url`, `issues_url` and `new_issue_url` will '\
- 'allow a user to easily navigate to the Jira issue tracker. See the '\
+ 'See the ' \
'[integration doc](http://doc.gitlab.com/ce/integration/external-issue-tracker.html) '\
'for details.'
end
@@ -53,12 +100,26 @@ class JiraService < IssueTrackerService
end
def fields
- super.push(
- { type: 'text', name: 'api_url', placeholder: 'https://jira.example.com/rest/api/2' },
+ [
+ { type: 'text', name: 'url', title: 'URL', placeholder: 'https://jira.example.com' },
+ { type: 'text', name: 'project_key', placeholder: 'Project Key' },
{ type: 'text', name: 'username', placeholder: '' },
{ type: 'password', name: 'password', placeholder: '' },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '2' }
- )
+ ]
+ end
+
+ # URLs to redirect from Gitlab issues pages to jira issue tracker
+ def project_url
+ "#{url}/issues/?jql=project=#{project_key}"
+ end
+
+ def issues_url
+ "#{url}/browse/:id"
+ end
+
+ def new_issue_url
+ "#{url}/secure/CreateIssue.jspa"
end
def execute(push, issue = nil)
@@ -72,7 +133,7 @@ class JiraService < IssueTrackerService
end
def create_cross_reference_note(mentioned, noteable, author)
- issue_name = mentioned.id
+ issue_key = mentioned.id
project = self.project
noteable_name = noteable.class.name.underscore.downcase
noteable_id = if noteable.is_a?(Commit)
@@ -99,58 +160,28 @@ class JiraService < IssueTrackerService
}
}
- add_comment(data, issue_name)
+ add_comment(data, issue_key)
end
def test_settings
- return unless api_url.present?
- result = JiraService.get(
- jira_api_test_url,
- headers: {
- 'Content-Type' => 'application/json',
- 'Authorization' => "Basic #{auth}"
- }
- )
+ return unless url.present?
+ # Test settings by getting the project
+ jira_project
- case result.code
- when 201, 200
- Rails.logger.info("#{self.class.name} SUCCESS #{result.code}: Successfully connected to #{api_url}.")
- true
- else
- Rails.logger.info("#{self.class.name} ERROR #{result.code}: #{result.parsed_response}")
- false
- end
- rescue Errno::ECONNREFUSED => e
- Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{api_url}."
+ rescue Errno::ECONNREFUSED, JIRA::HTTPError => e
+ Rails.logger.info "#{self.class.name} ERROR: #{e.message}. API URL: #{url}."
false
end
private
- def build_api_url_from_project_url
- server = URI(project_url)
- default_ports = [["http", 80], ["https", 443]].include?([server.scheme, server.port])
- server_url = "#{server.scheme}://#{server.host}"
- server_url.concat(":#{server.port}") unless default_ports
- "#{server_url}/rest/api/#{DEFAULT_API_VERSION}"
- rescue
- "" # looks like project URL was not valid
- end
-
- def set_api_url
- self.api_url = build_api_url_from_project_url if self.api_url.blank?
- end
-
- def set_jira_issue_transition_id
- self.jira_issue_transition_id ||= "2"
- end
-
def close_issue(entity, issue)
commit_id = if entity.is_a?(Commit)
entity.id
elsif entity.is_a?(MergeRequest)
entity.diff_head_sha
end
+
commit_url = build_entity_url(:commit, commit_id)
# Depending on the JIRA project's workflow, a comment during transition
@@ -161,24 +192,16 @@ class JiraService < IssueTrackerService
end
def transition_issue(issue)
- message = {
- transition: {
- id: jira_issue_transition_id
- }
- }
- send_message(close_issue_url(issue.iid), message.to_json)
+ issue = client.Issue.find(issue.iid)
+ issue.transitions.build.save(transition: { id: jira_issue_transition_id })
end
def add_issue_solved_comment(issue, commit_id, commit_url)
- comment = {
- body: "Issue solved with [#{commit_id}|#{commit_url}]."
- }
-
- send_message(comment_url(issue.iid), comment.to_json)
+ comment = "Issue solved with [#{commit_id}|#{commit_url}]."
+ send_message(issue.iid, comment)
end
- def add_comment(data, issue_name)
- url = comment_url(issue_name)
+ def add_comment(data, issue_key)
user_name = data[:user][:name]
user_url = data[:user][:url]
entity_name = data[:entity][:name]
@@ -186,68 +209,31 @@ class JiraService < IssueTrackerService
entity_title = data[:entity][:title]
project_name = data[:project][:name]
- message = {
- body: %Q{[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'}
- }
+ message = "[#{user_name}|#{user_url}] mentioned this issue in [a #{entity_name} of #{project_name}|#{entity_url}]:\n'#{entity_title}'"
- unless existing_comment?(issue_name, message[:body])
- send_message(url, message.to_json)
+ unless comment_exists?(issue_key, message)
+ send_message(issue_key, message)
end
end
- def auth
- require 'base64'
- Base64.urlsafe_encode64("#{self.username}:#{self.password}")
- end
-
- def send_message(url, message)
- return unless api_url.present?
- result = JiraService.post(
- url,
- body: message,
- headers: {
- 'Content-Type' => 'application/json',
- 'Authorization' => "Basic #{auth}"
- }
- )
-
- message = case result.code
- when 201, 200, 204
- "#{self.class.name} SUCCESS #{result.code}: Successfully posted to #{url}."
- when 401
- "#{self.class.name} ERROR 401: Unauthorized. Check the #{self.username} credentials and JIRA access permissions and try again."
- else
- "#{self.class.name} ERROR #{result.code}: #{result.parsed_response}"
- end
-
- Rails.logger.info(message)
- message
- rescue URI::InvalidURIError, Errno::ECONNREFUSED => e
- Rails.logger.info "#{self.class.name} ERROR: #{e.message}. Hostname: #{url}."
+ def comment_exists?(issue_key, message)
+ comments = client.Issue.find(issue_key).comments
+ comments.map { |comment| comment.body.include?(message) }.any?
end
- def existing_comment?(issue_name, new_comment)
- return unless api_url.present?
- result = JiraService.get(
- comment_url(issue_name),
- headers: {
- 'Content-Type' => 'application/json',
- 'Authorization' => "Basic #{auth}"
- }
- )
+ def send_message(issue_key, message)
+ return unless url.present?
- case result.code
- when 201, 200
- existing_comments = JSON.parse(result.body)['comments']
+ issue = client.Issue.find(issue_key)
- if existing_comments.present?
- return existing_comments.map { |comment| comment['body'].include?(new_comment) }.any?
- end
+ if issue.comments.build.save!(body: message)
+ result_message = "#{self.class.name} SUCCESS: Successfully posted to #{url}."
end
- false
- rescue JSON::ParserError
- false
+ Rails.logger.info(result_message)
+ result_message
+ rescue URI::InvalidURIError, Errno::ECONNREFUSED, JIRA::HTTPError => e
+ Rails.logger.info "#{self.class.name} Send message ERROR: #{url} - #{e.message}"
end
def resource_url(resource)
@@ -267,16 +253,4 @@ class JiraService < IssueTrackerService
)
)
end
-
- def close_issue_url(issue_name)
- "#{self.api_url}/issue/#{issue_name}/transitions"
- end
-
- def comment_url(issue_name)
- "#{self.api_url}/issue/#{issue_name}/comment"
- end
-
- def jira_api_test_url
- "#{self.api_url}/myself"
- end
end
diff --git a/app/models/project_services/redmine_service.rb b/app/models/project_services/redmine_service.rb
index f634e0772c0..f9da273cf08 100644
--- a/app/models/project_services/redmine_service.rb
+++ b/app/models/project_services/redmine_service.rb
@@ -1,4 +1,6 @@
class RedmineService < IssueTrackerService
+ validates :project_url, :issues_url, :new_issue_url, presence: true, url: true, if: :activated?
+
prop_accessor :title, :description, :project_url, :issues_url, :new_issue_url
def title
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 79d041d2775..a6e911df9bd 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -125,14 +125,8 @@ class ProjectTeam
max_member_access(user.id) == Gitlab::Access::MASTER
end
- def member?(user, min_member_access = nil)
- member = !!find_member(user.id)
-
- if min_member_access
- member && max_member_access(user.id) >= min_member_access
- else
- member
- end
+ def member?(user, min_member_access = Gitlab::Access::GUEST)
+ max_member_access(user.id) >= min_member_access
end
def human_max_access(user_id)
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 4ae9c20726f..30be7262438 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -11,6 +11,20 @@ class Repository
attr_accessor :path_with_namespace, :project
+ def self.storages
+ Gitlab.config.repositories.storages
+ end
+
+ def self.remove_storage_from_path(repo_path)
+ storages.find do |_, storage_path|
+ if repo_path.start_with?(storage_path)
+ return repo_path.sub(storage_path, '')
+ end
+ end
+
+ repo_path
+ end
+
def initialize(path_with_namespace, project)
@path_with_namespace = path_with_namespace
@project = project
@@ -181,7 +195,7 @@ class Repository
before_remove_branch
branch = find_branch(branch_name)
- oldrev = branch.try(:target).try(:id)
+ oldrev = branch.try(:dereferenced_target).try(:id)
newrev = Gitlab::Git::BLANK_SHA
ref = Gitlab::Git::BRANCH_REF_PREFIX + branch_name
@@ -297,10 +311,10 @@ class Repository
# Rugged seems to throw a `ReferenceError` when given branch_names rather
# than SHA-1 hashes
number_commits_behind = raw_repository.
- count_commits_between(branch.target.sha, root_ref_hash)
+ count_commits_between(branch.dereferenced_target.sha, root_ref_hash)
number_commits_ahead = raw_repository.
- count_commits_between(root_ref_hash, branch.target.sha)
+ count_commits_between(root_ref_hash, branch.dereferenced_target.sha)
{ behind: number_commits_behind, ahead: number_commits_ahead }
end
@@ -682,11 +696,11 @@ class Repository
branches.sort_by(&:name)
when 'updated_desc'
branches.sort do |a, b|
- commit(b.target).committed_date <=> commit(a.target).committed_date
+ commit(b.dereferenced_target).committed_date <=> commit(a.dereferenced_target).committed_date
end
when 'updated_asc'
branches.sort do |a, b|
- commit(a.target).committed_date <=> commit(b.target).committed_date
+ commit(a.dereferenced_target).committed_date <=> commit(b.dereferenced_target).committed_date
end
else
branches
@@ -861,7 +875,7 @@ class Repository
branch = find_branch(ref)
if branch
- last_commit = branch.target
+ last_commit = branch.dereferenced_target
index.read_tree(last_commit.raw_commit.tree)
parents = [last_commit.sha]
end
@@ -948,7 +962,7 @@ class Repository
end
def revert(user, commit, base_branch, revert_tree_id = nil)
- source_sha = find_branch(base_branch).target.sha
+ source_sha = find_branch(base_branch).dereferenced_target.sha
revert_tree_id ||= check_revert_content(commit, base_branch)
return false unless revert_tree_id
@@ -965,7 +979,7 @@ class Repository
end
def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
- source_sha = find_branch(base_branch).target.sha
+ source_sha = find_branch(base_branch).dereferenced_target.sha
cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
return false unless cherry_pick_tree_id
@@ -994,7 +1008,7 @@ class Repository
end
def check_revert_content(commit, base_branch)
- source_sha = find_branch(base_branch).target.sha
+ source_sha = find_branch(base_branch).dereferenced_target.sha
args = [commit.id, source_sha]
args << { mainline: 1 } if commit.merge_commit?
@@ -1008,7 +1022,7 @@ class Repository
end
def check_cherry_pick_content(commit, base_branch)
- source_sha = find_branch(base_branch).target.sha
+ source_sha = find_branch(base_branch).dereferenced_target.sha
args = [commit.id, source_sha]
args << 1 if commit.merge_commit?
@@ -1081,7 +1095,7 @@ class Repository
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)
+ oldrev = rugged.merge_base(newrev, target_branch.dereferenced_target.sha)
end
GitHooksService.new.execute(current_user, path_to_repo, oldrev, newrev, ref) do
@@ -1141,7 +1155,7 @@ class Repository
end
def tags_sorted_by_committed_date
- tags.sort_by { |tag| tag.target.committed_date }
+ tags.sort_by { |tag| tag.dereferenced_target.committed_date }
end
def keep_around_ref_name(sha)
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 11c072dd000..f5ade1cc293 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -53,7 +53,7 @@ class Todo < ActiveRecord::Base
# Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
def order_by_labels_priority
params = {
- target_type: ['Issue', 'MergeRequest'],
+ target_type_column: "todos.target_type",
target_column: "todos.target_id",
project_column: "todos.project_id"
}
diff --git a/app/models/user.rb b/app/models/user.rb
index 9e76df63d31..af3c0b7dc02 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -93,8 +93,10 @@ class User < ActiveRecord::Base
#
# Validations
#
+ # Note: devise :validatable above adds validations for :email and :password
validates :name, presence: true
- validates :notification_email, presence: true, email: true
+ validates :notification_email, presence: true
+ validates :notification_email, email: true, if: ->(user) { user.notification_email != user.email }
validates :public_email, presence: true, uniqueness: true, email: true, allow_blank: true
validates :bio, length: { maximum: 255 }, allow_blank: true
validates :projects_limit, presence: true, numericality: { greater_than_or_equal_to: 0 }
@@ -256,6 +258,24 @@ class User < ActiveRecord::Base
)
end
+ # searches user by given pattern
+ # it compares name, email, username fields and user's secondary emails with given pattern
+ # This method uses ILIKE on PostgreSQL and LIKE on MySQL.
+
+ def search_with_secondary_emails(query)
+ table = arel_table
+ email_table = Email.arel_table
+ pattern = "%#{query}%"
+ matched_by_emails_user_ids = email_table.project(email_table[:user_id]).where(email_table[:email].matches(pattern))
+
+ where(
+ table[:name].matches(pattern).
+ or(table[:email].matches(pattern)).
+ or(table[:username].matches(pattern)).
+ or(table[:id].in(matched_by_emails_user_ids))
+ )
+ end
+
def by_login(login)
return nil unless login
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index d3dd30b2588..8face432d97 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -10,17 +10,14 @@ module Ci
create_builds!
end
- @pipeline.with_lock do
- new_builds =
- stage_indexes_of_created_builds.map do |index|
- process_stage(index)
- end
+ new_builds =
+ stage_indexes_of_created_builds.map do |index|
+ process_stage(index)
+ end
- @pipeline.update_status
+ @pipeline.update_status
- # Return a flag if a when builds got enqueued
- new_builds.flatten.any?
- end
+ new_builds.flatten.any?
end
private
@@ -32,9 +29,11 @@ module Ci
def process_stage(index)
current_status = status_for_prior_stages(index)
- created_builds_in_stage(index).select do |build|
- if HasStatus::COMPLETED_STATUSES.include?(current_status)
- process_build(build, current_status)
+ if HasStatus::COMPLETED_STATUSES.include?(current_status)
+ created_builds_in_stage(index).select do |build|
+ Gitlab::OptimisticLocking.retry_lock(build) do |subject|
+ process_build(subject, current_status)
+ end
end
end
end
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index 6973191b203..74b5ebf372b 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -28,17 +28,14 @@ module Ci
if build
# In case when 2 runners try to assign the same build, second runner will be declined
- # with StateMachines::InvalidTransition in run! method.
- build.with_lock do
- build.runner_id = current_runner.id
- build.save!
- build.run!
- end
+ # with StateMachines::InvalidTransition or StaleObjectError when doing run! or save method.
+ build.runner_id = current_runner.id
+ build.run!
end
build
- rescue StateMachines::InvalidTransition
+ rescue StateMachines::InvalidTransition, ActiveRecord::StaleObjectError
nil
end
diff --git a/app/services/delete_branch_service.rb b/app/services/delete_branch_service.rb
index 918eddaa53a..3e5dd4ebb86 100644
--- a/app/services/delete_branch_service.rb
+++ b/app/services/delete_branch_service.rb
@@ -42,7 +42,7 @@ class DeleteBranchService < BaseService
Gitlab::DataBuilder::Push.build(
project,
current_user,
- branch.target.sha,
+ branch.dereferenced_target.sha,
Gitlab::Git::BLANK_SHA,
"#{Gitlab::Git::BRANCH_REF_PREFIX}#{branch.name}",
[])
diff --git a/app/services/delete_tag_service.rb b/app/services/delete_tag_service.rb
index d0cb151a010..d824406cb49 100644
--- a/app/services/delete_tag_service.rb
+++ b/app/services/delete_tag_service.rb
@@ -36,7 +36,7 @@ class DeleteTagService < BaseService
Gitlab::DataBuilder::Push.build(
project,
current_user,
- tag.target.sha,
+ tag.dereferenced_target.sha,
Gitlab::Git::BLANK_SHA,
"#{Gitlab::Git::TAG_REF_PREFIX}#{tag.name}",
[])
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index e6002b03b93..20a4445bddf 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -27,8 +27,8 @@ class GitTagPushService < BaseService
tag_name = Gitlab::Git.ref_name(params[:ref])
tag = project.repository.find_tag(tag_name)
- if tag && tag.object_sha == params[:newrev]
- commit = project.commit(tag.target)
+ if tag && tag.target == params[:newrev]
+ commit = project.commit(tag.dereferenced_target)
commits = [commit].compact
message = tag.message
end
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index 74291312c4e..d622f9edd33 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -2,21 +2,24 @@ module Labels
class FindOrCreateService
def initialize(current_user, project, params = {})
@current_user = current_user
- @group = project.group
@project = project
@params = params.dup
end
- def execute
+ def execute(skip_authorization: false)
+ @skip_authorization = skip_authorization
find_or_create_label
end
private
- attr_reader :current_user, :group, :project, :params
+ attr_reader :current_user, :project, :params, :skip_authorization
def available_labels
- @available_labels ||= LabelsFinder.new(current_user, project_id: project.id).execute
+ @available_labels ||= LabelsFinder.new(
+ current_user,
+ project_id: project.id
+ ).execute(skip_authorization: skip_authorization)
end
def find_or_create_label
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index 416aee2ab51..c13f289f61e 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -4,17 +4,25 @@ module Members
attr_accessor :source
+ # source - The source object that respond to `#requesters` (i.g. project or group)
+ # current_user - The user that performs the access request approval
+ # params - A hash of parameters
+ # :user_id - User ID used to retrieve the access requester
+ # :id - Member ID used to retrieve the access requester
+ # :access_level - Optional access level set when the request is accepted
def initialize(source, current_user, params = {})
@source = source
@current_user = current_user
- @params = params
+ @params = params.slice(:user_id, :id, :access_level)
end
- def execute
+ # opts - A hash of options
+ # :force - Bypass permission check: current_user can be nil in that case
+ def execute(opts = {})
condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
access_requester = source.requesters.find_by!(condition)
- raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester)
+ raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts)
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
@@ -24,8 +32,11 @@ module Members
private
- def can_update_access_requester?(access_requester)
- access_requester && can?(current_user, action_member_permission(:update, access_requester), access_requester)
+ def can_update_access_requester?(access_requester, opts = {})
+ access_requester && (
+ opts[:force] ||
+ can?(current_user, action_member_permission(:update, access_requester), access_requester)
+ )
end
end
end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
new file mode 100644
index 00000000000..e4b24ccef92
--- /dev/null
+++ b/app/services/members/create_service.rb
@@ -0,0 +1,16 @@
+module Members
+ class CreateService < BaseService
+ def execute
+ return false if params[:user_ids].blank?
+
+ project.team.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ expires_at: params[:expires_at],
+ current_user: current_user
+ )
+
+ true
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 404f75616b5..f415244068b 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -13,20 +13,8 @@ module MergeRequests
merge_request.target_project ||= (project.forked_from_project || project)
merge_request.target_branch ||= merge_request.target_project.default_branch
- if merge_request.target_branch.blank? || merge_request.source_branch.blank?
- message =
- if params[:source_branch] || params[:target_branch]
- "You must select source and target branch"
- end
-
- return build_failed(merge_request, message)
- end
-
- if merge_request.source_project == merge_request.target_project &&
- merge_request.target_branch == merge_request.source_branch
-
- return build_failed(merge_request, 'You must select different branches')
- end
+ messages = validate_branches(merge_request)
+ return build_failed(merge_request, messages) unless messages.empty?
compare = CompareService.new.execute(
merge_request.source_project,
@@ -43,6 +31,34 @@ module MergeRequests
private
+ def validate_branches(merge_request)
+ messages = []
+
+ if merge_request.target_branch.blank? || merge_request.source_branch.blank?
+ messages <<
+ if params[:source_branch] || params[:target_branch]
+ "You must select source and target branch"
+ end
+ end
+
+ if merge_request.source_project == merge_request.target_project &&
+ merge_request.target_branch == merge_request.source_branch
+
+ messages << 'You must select different branches'
+ end
+
+ # See if source and target branches exist
+ unless merge_request.source_project.commit(merge_request.source_branch)
+ messages << "Source branch \"#{merge_request.source_branch}\" does not exist"
+ end
+
+ unless merge_request.target_project.commit(merge_request.target_branch)
+ messages << "Target branch \"#{merge_request.target_branch}\" does not exist"
+ end
+
+ messages
+ end
+
# When your branch name starts with an iid followed by a dash this pattern will be
# interpreted as the user wants to close that issue on this project.
#
@@ -91,8 +107,10 @@ module MergeRequests
merge_request
end
- def build_failed(merge_request, message)
- merge_request.errors.add(:base, message) unless message.nil?
+ def build_failed(merge_request, messages)
+ messages.compact.each do |message|
+ merge_request.errors.add(:base, message)
+ end
merge_request.compare_commits = []
merge_request.can_be_created = false
merge_request
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index e466ffa60eb..d7221fe993c 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -29,7 +29,7 @@ module Projects
if unknown_url?
# In this case, we only want to import issues, not a repository.
create_repository
- else
+ elsif !project.repository_exists?
import_repository
end
end
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index adfa1eaafc9..05c88ca1cc8 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -16,7 +16,8 @@
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false)
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ .image-container.s40
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to [:admin, group], class: 'group-name' do
= group.name
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 0188ed448ce..a7c1a4f5038 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -13,7 +13,8 @@
Group info:
%ul.well-list
%li
- = image_tag group_icon(@group), class: "avatar s60"
+ .image-container.s60
+ = image_tag group_icon(@group), class: "avatar s60"
%li
%span.light Name:
%strong= @group.name
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 339cfc613fe..10dce6f3d8f 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -76,7 +76,8 @@
.title
= link_to [:admin, project.namespace.becomes(Namespace), project] do
.dash-project-avatar
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ .image-container.s40
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name
%span.namespace-name
- if project.namespace
diff --git a/app/views/admin/users/index.html.haml b/app/views/admin/users/index.html.haml
index 357123c2c13..d3038ae644f 100644
--- a/app/views/admin/users/index.html.haml
+++ b/app/views/admin/users/index.html.haml
@@ -10,7 +10,7 @@
= hidden_field_tag "filter", h(params[:filter])
.search-holder
.search-field-holder
- = search_field_tag :name, params[:name], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
+ = search_field_tag :search_query, params[:search_query], placeholder: 'Search by name, email or username', class: 'form-control search-text-input js-search-input', spellcheck: false
= icon("search", class: "search-icon")
.dropdown
- toggle_text = if @sort.present? then sort_options_hash[@sort] else sort_title_name end
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 2a0302638ba..2411cc45724 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -1,69 +1,70 @@
- page_title "Todos"
- header_title "Todos", dashboard_todos_path
-.top-area
- %ul.nav-links
- - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
- %li{class: "todos-pending #{todo_pending_active}"}
- = link_to todos_filter_path(state: 'pending') do
- %span
- To do
- %span.badge
- = number_with_delimiter(todos_pending_count)
- - todo_done_active = ('active' if params[:state] == 'done')
- %li{class: "todos-done #{todo_done_active}"}
- = link_to todos_filter_path(state: 'done') do
- %span
- Done
- %span.badge
- = number_with_delimiter(todos_done_count)
+- if current_user.todos.any?
+ .top-area
+ %ul.nav-links
+ - todo_pending_active = ('active' if params[:state].blank? || params[:state] == 'pending')
+ %li{class: "todos-pending #{todo_pending_active}"}
+ = link_to todos_filter_path(state: 'pending') do
+ %span
+ To do
+ %span.badge
+ = number_with_delimiter(todos_pending_count)
+ - todo_done_active = ('active' if params[:state] == 'done')
+ %li{class: "todos-done #{todo_done_active}"}
+ = link_to todos_filter_path(state: 'done') do
+ %span
+ Done
+ %span.badge
+ = number_with_delimiter(todos_done_count)
- .nav-controls
- - if @todos.any?(&:pending?)
- = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
- Mark all as done
- = icon('spinner spin')
+ .nav-controls
+ - if @todos.any?(&:pending?)
+ = link_to destroy_all_dashboard_todos_path(todos_filter_params), class: 'btn btn-loading js-todos-mark-all', method: :delete do
+ Mark all as done
+ = icon('spinner spin')
-.todos-filters
- .row-content-block.second-block
- = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
- .filter-item.inline
- - if params[:project_id].present?
- = hidden_field_tag(:project_id, params[:project_id])
- = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
- placeholder: 'Search projects', data: { data: todo_projects_options } })
- .filter-item.inline
- - if params[:author_id].present?
- = hidden_field_tag(:author_id, params[:author_id])
- = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
- placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } })
- .filter-item.inline
- - if params[:type].present?
- = hidden_field_tag(:type, params[:type])
- = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
- data: { data: todo_types_options } })
- .filter-item.inline.actions-filter
- - if params[:action_id].present?
- = hidden_field_tag(:action_id, params[:action_id])
- = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
- data: { data: todo_actions_options }})
- .pull-right
- .dropdown.inline.prepend-left-10
- %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
- %span.light
- - if @sort.present?
- = sort_options_hash[@sort]
- - else
- = sort_title_recently_created
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
- %li
- = link_to todos_filter_path(sort: sort_value_priority) do
- = sort_title_priority
- = link_to todos_filter_path(sort: sort_value_recently_created) do
+ .todos-filters
+ .row-content-block.second-block
+ = form_tag todos_filter_path(without: [:project_id, :author_id, :type, :action_id]), method: :get, class: 'filter-form' do
+ .filter-item.inline
+ - if params[:project_id].present?
+ = hidden_field_tag(:project_id, params[:project_id])
+ = dropdown_tag(project_dropdown_label(params[:project_id], 'Project'), options: { toggle_class: 'js-project-search js-filter-submit', title: 'Filter by project', filter: true, filterInput: 'input#project-search', dropdown_class: 'dropdown-menu-selectable dropdown-menu-project js-filter-submit',
+ placeholder: 'Search projects', data: { data: todo_projects_options } })
+ .filter-item.inline
+ - if params[:author_id].present?
+ = hidden_field_tag(:author_id, params[:author_id])
+ = dropdown_tag(user_dropdown_label(params[:author_id], 'Author'), options: { toggle_class: 'js-user-search js-filter-submit js-author-search', title: 'Filter by author', filter: true, filterInput: 'input#author-search', dropdown_class: 'dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit',
+ placeholder: 'Search authors', data: { any_user: 'Any Author', first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: 'author_id', default_label: 'Author' } })
+ .filter-item.inline
+ - if params[:type].present?
+ = hidden_field_tag(:type, params[:type])
+ = dropdown_tag(todo_types_dropdown_label(params[:type], 'Type'), options: { toggle_class: 'js-type-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-type js-filter-submit',
+ data: { data: todo_types_options } })
+ .filter-item.inline.actions-filter
+ - if params[:action_id].present?
+ = hidden_field_tag(:action_id, params[:action_id])
+ = dropdown_tag(todo_actions_dropdown_label(params[:action_id], 'Action'), options: { toggle_class: 'js-action-search js-filter-submit', dropdown_class: 'dropdown-menu-selectable dropdown-menu-action js-filter-submit',
+ data: { data: todo_actions_options }})
+ .pull-right
+ .dropdown.inline.prepend-left-10
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
= sort_title_recently_created
- = link_to todos_filter_path(sort: sort_value_oldest_created) do
- = sort_title_oldest_created
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %li
+ = link_to todos_filter_path(sort: sort_value_priority) do
+ = sort_title_priority
+ = link_to todos_filter_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to todos_filter_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
.prepend-top-default
@@ -78,5 +79,29 @@
%ul.content-list.todos-list
= render group[1]
= paginate @todos, theme: "gitlab"
+ - elsif current_user.todos.any?
+ .todos-all-done
+ = render "shared/empty_states/todos_all_done.svg"
+ %h4.text-center
+ Good job! Looks like you don't have any todos left.
+ %p.text-center
+ Are you looking for things to do? Take a look at
+ = succeed "," do
+ = link_to "the opened issues", issues_dashboard_path
+ contribute to
+ = link_to "merge requests", merge_requests_dashboard_path
+ or mention someone in a comment to assign a new todo automatically.
- else
- .nothing-here-block You're all done!
+ .todos-empty
+ .todos-empty-hero
+ = render "shared/empty_states/todos_empty.svg"
+ .todos-empty-content
+ %h4
+ Todos let you see what you should do next.
+ %p
+ When an issue or merge request is assigned to you, or when you
+ %strong
+ @mention
+ in a comment, this will trigger a new item in your todo list, automatically.
+ %p
+ You will always know what to work on next.
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index c766370d5a0..f84ac37fa8f 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -8,7 +8,8 @@
.form-group
.col-sm-offset-2.col-sm-10
- = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
+ .image-container.s160
+ = image_tag group_icon(@group), alt: '', class: 'avatar group-avatar s160'
%p.light
- if @group.avatar?
You can change your group avatar here
diff --git a/app/views/groups/labels/index.html.haml b/app/views/groups/labels/index.html.haml
index 70783a63409..45325d6bc4b 100644
--- a/app/views/groups/labels/index.html.haml
+++ b/app/views/groups/labels/index.html.haml
@@ -13,7 +13,7 @@
.other-labels
- if @labels.present?
%ul.content-list.manage-labels-list.js-other-labels
- = render partial: 'shared/label', collection: @labels, as: :label
+ = render partial: 'shared/label', subject: @group, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- else
.nothing-here-block
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index fab61f447c2..275581b3af8 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -6,7 +6,8 @@
.cover-block.groups-cover-block
%div{ class: container_class }
- = image_tag group_icon(@group), class: "avatar group-avatar s70 avatar-tile"
+ .image-container.s70.group-avatar
+ = image_tag group_icon(@group), class: "avatar s70 avatar-tile"
.group-info
.cover-title
%h1
diff --git a/app/views/help/ui.html.haml b/app/views/help/ui.html.haml
index d16bd61b779..070ed90da6d 100644
--- a/app/views/help/ui.html.haml
+++ b/app/views/help/ui.html.haml
@@ -461,7 +461,7 @@
.panel-body
= lorem
- %h2#alert Alerts
+ %h2#alerts Alerts
.row
.col-md-6
diff --git a/app/views/kaminari/gitlab/_gap.html.haml b/app/views/kaminari/gitlab/_gap.html.haml
index 80ca30f36e6..889514c4755 100644
--- a/app/views/kaminari/gitlab/_gap.html.haml
+++ b/app/views/kaminari/gitlab/_gap.html.haml
@@ -4,6 +4,6 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{class: "page"}
- %span.page.gap
+%li
+ %span.gap
= raw(t 'views.pagination.truncate')
diff --git a/app/views/kaminari/gitlab/_page.html.haml b/app/views/kaminari/gitlab/_page.html.haml
index 522e4d1d05f..750aed8f329 100644
--- a/app/views/kaminari/gitlab/_page.html.haml
+++ b/app/views/kaminari/gitlab/_page.html.haml
@@ -6,5 +6,5 @@
-# total_pages: total number of pages
-# per_page: number of items to fetch per page
-# remote: data-remote
-%li{class: "page#{' active' if page.current?}"}
+%li{class: "page#{' active' if page.current?}#{' sibling' if page.next? || page.prev?}"}
= link_to page, url, {remote: remote, rel: page.next? ? 'next' : page.prev? ? 'prev' : nil}
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 7faa8bded86..7a9859262f7 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -29,10 +29,6 @@
= icon('bell fw')
%span.badge.todos-pending-count{ class: ("hidden" if todos_pending_count == 0) }
= todos_pending_count
- - if current_user.can_create_project?
- %li
- = link_to new_project_path, title: 'New project', aria: { label: "New project" }, data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
- = icon('plus fw')
- if Gitlab::Sherlock.enabled?
%li
= link_to sherlock_transactions_path, title: 'Sherlock Transactions',
@@ -48,6 +44,8 @@
= link_to "Profile", current_user, class: 'profile-link', aria: { label: "Profile" }, data: { user: current_user.username }
%li
= link_to "Profile Settings", profile_path, aria: { label: "Profile Settings" }
+ %li
+ = link_to "Help", help_path, aria: { label: "Help" }
%li.divider
%li
= link_to "Sign out", destroy_user_session_path, method: :delete, class: "sign-out-link", aria: { label: "Sign out" }
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 67f558c854b..a0356feef95 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,49 +1,38 @@
-%ul.nav.nav-sidebar
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
- = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
- %span
- Projects
- = nav_link(controller: :todos) do
- = link_to dashboard_todos_path, title: 'Todos' do
- %span
- Todos
- %span.count= number_with_delimiter(todos_pending_count)
- = nav_link(path: 'dashboard#activity') do
- = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
- %span
- Activity
- - if koding_enabled?
- = nav_link(controller: :koding) do
- = link_to koding_path, title: 'Koding' do
+.nav-sidebar
+ .sidebar-header Across GitLab
+ %ul.nav
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: {class: "#{project_tab_class} home"}) do
+ = link_to dashboard_projects_path, title: 'Projects', class: 'dashboard-shortcuts-projects' do
%span
- Koding
- = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
- = link_to dashboard_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link(controller: 'dashboard/milestones') do
- = link_to dashboard_milestones_path, title: 'Milestones' do
- %span
- Milestones
- = nav_link(path: 'dashboard#issues') do
- = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
- %span
- Issues
- %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
- = nav_link(path: 'dashboard#merge_requests') do
- = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
- %span
- Merge Requests
- %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
- = nav_link(controller: 'dashboard/snippets') do
- = link_to dashboard_snippets_path, title: 'Snippets' do
- %span
- Snippets
- = nav_link(controller: :help) do
- = link_to help_path, title: 'Help' do
- %span
- Help
- = nav_link(html_options: {class: profile_tab_class}) do
- = link_to profile_path, title: 'Profile Settings', data: {placement: 'bottom'} do
- %span
- Profile Settings
+ Projects
+ = nav_link(path: 'dashboard#activity') do
+ = link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
+ %span
+ Activity
+ - if koding_enabled?
+ = nav_link(controller: :koding) do
+ = link_to koding_path, title: 'Koding' do
+ %span
+ Koding
+ = nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
+ = link_to dashboard_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link(controller: 'dashboard/milestones') do
+ = link_to dashboard_milestones_path, title: 'Milestones' do
+ %span
+ Milestones
+ = nav_link(path: 'dashboard#issues') do
+ = link_to assigned_issues_dashboard_path, title: 'Issues', class: 'dashboard-shortcuts-issues' do
+ %span
+ Issues
+ %span.count= number_with_delimiter(current_user.assigned_issues.opened.count)
+ = nav_link(path: 'dashboard#merge_requests') do
+ = link_to assigned_mrs_dashboard_path, title: 'Merge Requests', class: 'dashboard-shortcuts-merge_requests' do
+ %span
+ Merge Requests
+ %span.count= number_with_delimiter(current_user.assigned_merge_requests.opened.count)
+ = nav_link(controller: 'dashboard/snippets') do
+ = link_to dashboard_snippets_path, title: 'Snippets' do
+ %span
+ Snippets
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index d3987fc9c4f..e67b66d1fff 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -1,7 +1,8 @@
- empty_repo = @project.empty_repo?
.project-home-panel.text-center{ class: ("empty-project" if empty_repo) }
%div{ class: container_class }
- = project_icon(@project, alt: @project.name, class: 'project-avatar avatar s70 avatar-tile')
+ .image-container.s70.project-avatar
+ = project_icon(@project, alt: @project.name, class: 'avatar s70 avatar-tile')
%h1.project-title
= @project.name
%span.visibility-icon.has-tooltip{data: { container: 'body' }, title: visibility_icon_description(@project)}
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index ba1502c97b6..f7071051efc 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -11,7 +11,9 @@
.board-inner
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
- {{ list.title }}
+ %span.has-tooltip{ ":title" => "(list.label ? list.label.description : '')",
+ data: { container: "body", placement: "bottom" } }
+ {{ list.title }}
.board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
%span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
{{ list.issuesSize }}
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index c6d718a1cd1..8fce702314c 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -7,8 +7,11 @@
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }",
- ":index" => "index" }
+ %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id, 'is-active': issueDetailVisible }",
+ ":index" => "index",
+ "@mousedown" => "mouseDown",
+ "@mouseMove" => "mouseMove",
+ "@mouseup" => "showIssue($event)" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id",
@@ -18,6 +21,11 @@
%span.card-number{ "v-if" => "issue.id" }
= precede '#' do
{{ issue.id }}
+ %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
+ ":title" => "'Assigned to ' + issue.assignee.name",
+ "v-if" => "issue.assignee",
+ data: { container: 'body' } }
+ %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
type: "button",
"v-if" => "(!list.label || label.id !== list.label.id)",
@@ -26,8 +34,3 @@
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- %a.has-tooltip{ ":href" => "'#{root_path}' + issue.assignee.username",
- ":title" => "'Assigned to ' + issue.assignee.name",
- "v-if" => "issue.assignee",
- data: { container: 'body' } }
- %img.avatar.avatar-inline.s20{ ":src" => "issue.assignee.avatar", width: 20, height: 20 }
diff --git a/app/views/projects/boards/components/_sidebar.html.haml b/app/views/projects/boards/components/_sidebar.html.haml
new file mode 100644
index 00000000000..f0c0c6953e0
--- /dev/null
+++ b/app/views/projects/boards/components/_sidebar.html.haml
@@ -0,0 +1,23 @@
+%board-sidebar{ "inline-template" => true,
+ ":current-user" => "#{current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) if current_user}" }
+ %aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
+ .issuable-sidebar
+ .block.issuable-sidebar-header
+ %span.issuable-header-text.hide-collapsed.pull-left
+ %strong
+ {{ issue.title }}
+ %br/
+ %span
+ = precede "#" do
+ {{ issue.id }}
+ %a.gutter-toggle.pull-right{ role: "button",
+ href: "#",
+ "@click.prevent" => "closeSidebar",
+ "aria-label" => "Toggle sidebar" }
+ = custom_icon("icon_close", size: 15)
+ .js-issuable-update
+ = render "projects/boards/components/sidebar/assignee"
+ = render "projects/boards/components/sidebar/milestone"
+ = render "projects/boards/components/sidebar/due_date"
+ = render "projects/boards/components/sidebar/labels"
+ = render "projects/boards/components/sidebar/notifications"
diff --git a/app/views/projects/boards/components/sidebar/_assignee.html.haml b/app/views/projects/boards/components/sidebar/_assignee.html.haml
new file mode 100644
index 00000000000..604e13858d1
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_assignee.html.haml
@@ -0,0 +1,40 @@
+.block.assignee
+ .title.hide-collapsed
+ Assignee
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value.hide-collapsed
+ %span.assign-yourself.no-value{ "v-if" => "!issue.assignee" }
+ No assignee
+ - if can?(current_user, :admin_issue, @project)
+ \-
+ %a.js-assign-yourself{ href: "#" }
+ assign yourself
+ %a.author_link.bold{ ":href" => "'#{root_url}' + issue.assignee.username",
+ "v-if" => "issue.assignee" }
+ %img.avatar.avatar-inline.s32{ ":src" => "issue.assignee.avatar",
+ width: "32" }
+ %span.author
+ {{ issue.assignee.name }}
+ %span.username
+ = precede "@" do
+ {{ issue.assignee.username }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox.hide-collapsed
+ %input{ type: "hidden",
+ name: "issue[assignee_id]",
+ id: "issue_assignee_id",
+ ":value" => "issue.assignee.id",
+ "v-if" => "issue.assignee" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-user-search.js-author-search.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", field_name: "issue[assignee_id]", first_user: (current_user.username if current_user), current_user: "true", project_id: @project.id, null_user: "true" },
+ ":data-issuable-id" => "issue.id",
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ Select assignee
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-menu-user.dropdown-menu-selectable.dropdown-menu-author
+ = dropdown_title("Assign to")
+ = dropdown_filter("Search users")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_due_date.html.haml b/app/views/projects/boards/components/sidebar/_due_date.html.haml
new file mode 100644
index 00000000000..c7da1d0d4ac
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_due_date.html.haml
@@ -0,0 +1,32 @@
+.block.due_date
+ .title
+ Due date
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value
+ .value-content
+ %span.no-value{ "v-if" => "!issue.dueDate" }
+ No due date
+ %span.bold{ "v-if" => "issue.dueDate" }
+ {{ issue.dueDate | due-date }}
+ - if can?(current_user, :admin_issue, @project)
+ %span.no-value.js-remove-due-date-holder{ "v-if" => "issue.dueDate" }
+ \-
+ %a.js-remove-due-date{ href: "#", role: "button" }
+ remove due date
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ name: "issue[due_date]",
+ ":value" => "issue.dueDate" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-due-date-select.js-issue-boards-due-date{ type: 'button',
+ data: { toggle: 'dropdown', field_name: "issue[due_date]", ability_name: "issue" },
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ %span.dropdown-toggle-text Due date
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-menu-due-date
+ = dropdown_title('Due date')
+ = dropdown_content do
+ .js-due-date-calendar
diff --git a/app/views/projects/boards/components/sidebar/_labels.html.haml b/app/views/projects/boards/components/sidebar/_labels.html.haml
new file mode 100644
index 00000000000..ce68e5e1998
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_labels.html.haml
@@ -0,0 +1,30 @@
+.block.labels
+ .title
+ Labels
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value.issuable-show-labels
+ %span.no-value{ "v-if" => "issue.labels && issue.labels.length === 0" }
+ None
+ %a{ href: "#",
+ "v-for" => "label in issue.labels" }
+ %span.label.color-label.has-tooltip{ ":style" => "{ backgroundColor: label.color, color: label.textColor }" }
+ {{ label.title }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ name: "issue[label_names][]",
+ "v-for" => "label in issue.labels",
+ ":value" => "label.id" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-issue-board-sidebar{ type: "button",
+ data: { toggle: "dropdown", field_name: "issue[label_names][]", show_no: "true", show_any: "true", project_id: @project.id, labels: namespace_project_labels_path(@project.namespace, @project, :json), namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) },
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ %span.dropdown-toggle-text
+ Label
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ = render partial: "shared/issuable/label_page_default"
+ - if can? current_user, :admin_label, @project and @project
+ = render partial: "shared/issuable/label_page_create"
diff --git a/app/views/projects/boards/components/sidebar/_milestone.html.haml b/app/views/projects/boards/components/sidebar/_milestone.html.haml
new file mode 100644
index 00000000000..3cd20d1c0f7
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_milestone.html.haml
@@ -0,0 +1,28 @@
+.block.milestone
+ .title
+ Milestone
+ = icon("spinner spin", class: "block-loading")
+ - if can?(current_user, :admin_issue, @project)
+ = link_to "Edit", "#", class: "edit-link pull-right"
+ .value
+ %span.no-value{ "v-if" => "!issue.milestone" }
+ None
+ %span.bold.has-tooltip{ "v-if" => "issue.milestone" }
+ {{ issue.milestone.title }}
+ - if can?(current_user, :admin_issue, @project)
+ .selectbox
+ %input{ type: "hidden",
+ ":value" => "issue.milestone.id",
+ name: "issue[milestone_id]",
+ "v-if" => "issue.milestone" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-milestone-select.js-issue-board-sidebar{ type: "button", data: { toggle: "dropdown", show_no: "true", field_name: "issue[milestone_id]", project_id: @project.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: "issue", use_id: "true" },
+ ":data-issuable-id" => "issue.id",
+ ":data-issue-update" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '.json'" }
+ Milestone
+ = icon("chevron-down")
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ = dropdown_title("Assignee milestone")
+ = dropdown_filter("Search milestones")
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/projects/boards/components/sidebar/_notifications.html.haml b/app/views/projects/boards/components/sidebar/_notifications.html.haml
new file mode 100644
index 00000000000..21c9563e9db
--- /dev/null
+++ b/app/views/projects/boards/components/sidebar/_notifications.html.haml
@@ -0,0 +1,11 @@
+- if current_user
+ .block.light.subscription{ ":data-url" => "'#{namespace_project_issues_path(@project.namespace, @project)}/' + issue.id + '/toggle_subscription'" }
+ .title
+ Notifications
+ %button.btn.btn-block.btn-default.js-subscribe-button.issuable-subscribe-button.hide-collapsed{ type: "button" }
+ {{ issue.subscribed ? 'Unsubscribe' : 'Subscribe' }}
+ .subscription-status{ ":data-status" => "issue.subscribed ? 'subscribed' : 'unsubscribed'" }
+ .unsubscribed{ "v-show" => "!issue.subscribed" }
+ You're not receiving notifications from this thread.
+ .subscribed{ "v-show" => "issue.subscribed" }
+ You're receiving notifications because you're subscribed to this thread.
diff --git a/app/views/projects/boards/index.html.haml b/app/views/projects/boards/index.html.haml
index 885f8e34b55..29c9a43a0c1 100644
--- a/app/views/projects/boards/index.html.haml
+++ b/app/views/projects/boards/index.html.haml
@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards
-.boards-list#board-app{ "v-cloak" => true, data: board_data }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/boards/show.html.haml b/app/views/projects/boards/show.html.haml
index 885f8e34b55..29c9a43a0c1 100644
--- a/app/views/projects/boards/show.html.haml
+++ b/app/views/projects/boards/show.html.haml
@@ -10,7 +10,9 @@
= render 'shared/issuable/filter', type: :boards
-.boards-list#board-app{ "v-cloak" => true, data: board_data }
- .boards-app-loading.text-center{ "v-if" => "loading" }
- = icon("spinner spin")
- = render "projects/boards/components/board"
+#board-app.boards-app{ "v-cloak" => true, data: board_data }
+ .boards-list{ ":class" => "{ 'is-compact': detailIssueVisible }" }
+ .boards-app-loading.text-center{ "v-if" => "loading" }
+ = icon("spinner spin")
+ = render "projects/boards/components/board"
+ = render "projects/boards/components/sidebar"
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 4480b2f22c3..9135cee8364 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -1,4 +1,4 @@
-- commit = @repository.commit(branch.target)
+- commit = @repository.commit(branch.dereferenced_target)
- bar_graph_width_factor = @max_commits > 0 ? 100.0/@max_commits : 0
- diverging_commit_counts = @repository.diverging_commit_counts(branch)
- number_commits_behind = diverging_commit_counts[:behind]
@@ -31,7 +31,12 @@
= render 'projects/buttons/download', project: @project, ref: branch.name
- if can?(current_user, :push_code, @project)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: "btn btn-remove remove-row has-tooltip #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}", title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name),
+ class: "btn btn-remove remove-row #{can_remove_branch?(@project, branch.name) ? '' : 'disabled'}",
+ method: :delete,
+ data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?" },
+ remote: true,
+ "aria-label" => "Delete branch" do
= icon("trash-o")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/builds/_header.html.haml b/app/views/projects/builds/_header.html.haml
index 51b5bd9db42..3f2ce7377fd 100644
--- a/app/views/projects/builds/_header.html.haml
+++ b/app/views/projects/builds/_header.html.haml
@@ -1,16 +1,19 @@
.content-block.build-header
- = ci_status_with_icon(@build.status)
- Build
- %strong ##{@build.id}
- for commit
- = link_to ci_status_path(@build.pipeline) do
- %strong= @build.pipeline.short_sha
- from
- = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
- %code
- = @build.ref
- - if @build.user
- = render "user"
- = time_ago_with_tooltip(@build.created_at)
+ .header-content
+ = ci_status_with_icon(@build.status)
+ Build
+ %strong ##{@build.id}
+ for commit
+ = link_to ci_status_path(@build.pipeline) do
+ %strong= @build.pipeline.short_sha
+ from
+ = link_to namespace_project_commits_path(@project.namespace, @project, @build.ref) do
+ %code
+ = @build.ref
+ - if @build.user
+ = render "user"
+ = time_ago_with_tooltip(@build.created_at)
+ - if can?(current_user, :update_build, @build) && @build.retryable?
+ = link_to "Retry build", retry_namespace_project_build_path(@project.namespace, @project, @build), class: 'btn btn-inverted pull-right', method: :post
%button.btn.btn-default.pull-right.visible-xs-block.visible-sm-block.build-gutter-toggle.js-sidebar-build-toggle{ role: "button", type: "button" }
= icon('angle-double-left')
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index b1053028279..28f519f11b2 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -44,7 +44,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 retry-link', method: :post
+ = link_to "Retry build", 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:
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 29d549a60f5..27da86b9efe 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -5,10 +5,10 @@
= custom_icon('icon_fork')
%span Fork
- else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn' do
= custom_icon('icon_fork')
%span Fork
%div.count-with-arrow
%span.arrow
- = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do
+ = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count' do
= @project.forks_count
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index 311583037e5..12d35101770 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -1,5 +1,5 @@
- if current_user
- = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star has-tooltip', method: :post, remote: true, title: current_user.starred?(@project) ? 'Unstar project' : 'Star project' } do
+ = link_to toggle_star_namespace_project_path(@project.namespace, @project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project)
= icon('star')
%span.starred Unstar
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 55965172d3f..93dca81e6f9 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -1,14 +1,13 @@
- is_playable = subject.playable? && can?(current_user, :update_build, @project)
- if is_playable
= link_to play_namespace_project_build_path(subject.project.namespace, subject.project, subject, return_to: request.original_url), method: :post, data: { toggle: 'tooltip', title: "#{subject.name} - play", container: '.pipeline-graph', placement: 'bottom' } do
- = render_status_with_link('build', 'play')
+ = ci_icon_for_status('play')
.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject), data: { toggle: 'tooltip', title: "#{subject.name} - #{subject.status}", container: '.pipeline-graph', placement: 'bottom' } do
%span.ci-status-icon
- = render_status_with_link('build', subject.status)
+ = ci_icon_for_status(subject.status)
.ci-status-text= subject.name
- else
%span.ci-status-icon
- = render_status_with_link('build', subject.status)
- = ci_icon_for_status(subject.status)
+ = ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index c6f359f5679..840f468dc05 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -5,10 +5,7 @@
%tr.commit
%td.commit-link
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- - if defined?(status_icon_only) && status_icon_only
- = ci_icon_for_status(status)
- - else
- = ci_status_with_icon(status)
+ = ci_status_with_icon(status)
%td
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
@@ -40,7 +37,7 @@
%p.commit-title
- if commit = pipeline.commit
= author_avatar(commit, size: 20)
- = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
+ = link_to_gfm truncate(commit.title, length: 60, escape: false), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 6c82a4e5600..d8c95376b94 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -1,8 +1,23 @@
.commit-info-row.commit-info-row-header
- %span.hidden-xs Authored by
+ %span.hidden-xs.hidden-sm Commit
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace js-details-short"
+ = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ %span.text-expander
+ \...
+ %span.js-details-content.hide
+ = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm"
+ = clipboard_button(clipboard_text: @commit.id)
+ %span.hidden-xs authored
+ #{time_ago_with_tooltip(@commit.authored_date)}
+ %span by
+ = author_avatar(@commit, size: 24)
%strong
= commit_author_link(@commit, avatar: true, size: 24)
- #{time_ago_with_tooltip(@commit.authored_date)}
+ - if @commit.different_committer?
+ %span.light Committed by
+ %strong
+ = commit_committer_link(@commit, avatar: true, size: 24)
+ #{time_ago_with_tooltip(@commit.committed_date)}
.pull-right.commit-action-buttons
- if defined?(@notes_count) && @notes_count > 0
@@ -33,42 +48,35 @@
%li= link_to "Email Patches", namespace_project_commit_path(@project.namespace, @project, @commit, format: :patch)
%li= link_to "Plain Diff", namespace_project_commit_path(@project.namespace, @project, @commit, format: :diff)
-- if @commit.different_committer?
- .commit-info-row
- %span.light Committed by
- %strong
- = commit_committer_link(@commit, avatar: true, size: 24)
- #{time_ago_with_tooltip(@commit.committed_date)}
-
-.commit-info-row
- %span.hidden-xs.hidden-sm Commit
- = link_to @commit.id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace hidden-xs hidden-sm"
- = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace visible-xs-inline visible-sm-inline"
- = clipboard_button(clipboard_text: @commit.id)
- %span.cgray= pluralize(@commit.parents.count, "parent")
- - @commit.parents.each do |parent|
- = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace"
-
- %span.commit-info.branches
- %i.fa.fa-spinner.fa-spin
-
-- if @commit.status
- .commit-info-row
- Builds for
- = pluralize(@commit.pipelines.count, 'pipeline')
- = link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "ci-status-link ci-status-icon-#{@commit.status}" do
- = ci_icon_for_status(@commit.status)
- %span.ci-status-label
- = ci_label_for_status(@commit.status)
- in
- = time_interval_in_words @commit.pipelines.total_duration
-
-.commit-box.content-block
+.commit-box
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line, author: @commit.author)
- if @commit.description.present?
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
+.commit-info-widget
+ .widget-row.branch-info
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+ %span.cgray= pluralize(@commit.parents.count, "parent")
+ - @commit.parents.each do |parent|
+ = link_to parent.short_id, namespace_project_commit_path(@project.namespace, @project, parent), class: "monospace"
+ %span.commit-info.branches
+ %i.fa.fa-spinner.fa-spin
+
+ - if @commit.status
+ .widget-row.pipeline-info
+ .icon-container
+ = ci_icon_for_status(@commit.status)
+ Pipeline
+ = link_to "##{@commit.pipelines.last.id}", pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id), class: "monospace"
+ for
+ = link_to @commit.short_id, namespace_project_commit_path(@project.namespace, @project, @commit), class: "monospace"
+ %span.ci-status-label
+ = ci_label_for_status(@commit.status)
+ in
+ = time_interval_in_words @commit.pipelines.total_duration
+
:javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
index f2d71fa6989..18daa2ee693 100644
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -1,7 +1,7 @@
- group_status = CommitStatus.where(id: subject).status
%button.dropdown-menu-toggle.has-tooltip{ type: 'button', data: { toggle: 'dropdown', title: "#{name} - #{group_status}" } }
%span.ci-status-icon
- = render_status_with_link('build', group_status)
+ = ci_icon_for_status(group_status)
%span.ci-status-text
= name
%span.badge= subject.size
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index ac451441eec..2dc91a9b762 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -12,4 +12,4 @@
%th Stages
%th
%th
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, show_commit: false
+ = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, show_commit: false
diff --git a/app/views/projects/commit/builds.html.haml b/app/views/projects/commit/builds.html.haml
index f9d7eac3542..077b2d2725b 100644
--- a/app/views/projects/commit/builds.html.haml
+++ b/app/views/projects/commit/builds.html.haml
@@ -3,8 +3,7 @@
= render "projects/commits/head"
%div{ class: container_class }
- .prepend-top-default
- = render "commit_box"
+ = render "commit_box"
= render "ci_menu"
= render "builds"
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
index d85d6729a81..8233e26e4e7 100644
--- a/app/views/projects/commit/pipelines.html.haml
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -1,7 +1,6 @@
- page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits"
-.prepend-top-default
- = render "commit_box"
+= render "commit_box"
= render "ci_menu"
= render "pipelines_list", pipelines: @ci_pipelines
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index cebf58d63df..b8c64d1f13e 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -4,8 +4,7 @@
= render "projects/commits/head"
%div{ class: container_class }
- .prepend-top-default
- = render "commit_box"
+ = render "commit_box"
- if @commit.status
= render "ci_menu"
- else
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index b647882efa0..247d612ba6f 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -1,5 +1,9 @@
- @no_container = true
- page_title "Cycle Analytics"
+
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('cycle_analytics/cycle_analytics_bundle.js')
+
= render "projects/pipelines/head"
#cycle-analytics{class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) }}
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 257e0a855bd..8f4f9ad4a80 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -8,7 +8,6 @@
= link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
\
- = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-file-option')
- if editable_diff?(diff_file)
- link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index a6a2e5690b5..73993f35b39 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -21,6 +21,8 @@
- if diff_file.deleted_file
deleted
+ = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-clipboard btn-transparent prepend-left-5', title: 'Copy filename to clipboard')
+
- if diff_file.mode_changed?
%small
= "#{diff_file.a_mode} → #{diff_file.b_mode}"
diff --git a/app/views/projects/diffs/_parallel_view.html.haml b/app/views/projects/diffs/_parallel_view.html.haml
index 28aad3f4725..78aa9fb7391 100644
--- a/app/views/projects/diffs/_parallel_view.html.haml
+++ b/app/views/projects/diffs/_parallel_view.html.haml
@@ -1,5 +1,5 @@
/ Side-by-side diff view
-%div.text-file.diff-wrap-lines.code.file-content.js-syntax-highlight{ data: diff_view_data }
+%div.text-file.diff-wrap-lines.code.js-syntax-highlight{ data: diff_view_data }
%table
- last_line = 0
- diff_file.parallel_diff_lines.each do |line|
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 30473d14b9b..ff6a899b92a 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -118,7 +118,8 @@
Project avatar
.form-group
- if @project.avatar?
- = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
+ .image-container.s160
+ = project_icon("#{@project.namespace.to_param}/#{@project.to_param}", alt: '', class: 'avatar project-avatar s160')
%p.light
- if @project.avatar_in_git
Project avatar in repository: #{ @project.avatar_in_git }
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index c45b73e4225..1c457244a7a 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -2,9 +2,9 @@
- if subject.target_url
= link_to subject.target_url do
%span.ci-status-icon
- = render_status_with_link('commit status', subject.status)
+ = ci_icon_for_status(subject.status)
%span.ci-status-text= subject.name
- else
%span.ci-status-icon
- = render_status_with_link('commit status', subject.status)
+ = ci_icon_for_status(subject.status)
%span.ci-status-text= subject.name
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 44683c8bcdb..1892ebb512f 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -4,7 +4,7 @@
%ul.unstyled-list.related-merge-requests
- @related_branches.each do |branch|
%li
- - target = @project.repository.find_branch(branch).target
+ - target = @project.repository.find_branch(branch).dereferenced_target
- pipeline = @project.pipeline_for(branch, target.sha) if target
- if pipeline
%span.related-branch-ci-status
diff --git a/app/views/projects/labels/index.html.haml b/app/views/projects/labels/index.html.haml
index f135bf6f6b4..05a8475dcd6 100644
--- a/app/views/projects/labels/index.html.haml
+++ b/app/views/projects/labels/index.html.haml
@@ -22,14 +22,14 @@
%ul.content-list.manage-labels-list.js-prioritized-labels{ "data-url" => set_priorities_namespace_project_labels_path(@project.namespace, @project) }
%p.empty-message{ class: ('hidden' unless @prioritized_labels.empty?) } No prioritized labels yet
- if @prioritized_labels.present?
- = render partial: 'shared/label', collection: @prioritized_labels, as: :label
+ = render partial: 'shared/label', subject: @project, collection: @prioritized_labels, as: :label
.other-labels
- if can?(current_user, :admin_label, @project)
%h5{ class: ('hide' if hide) } Other Labels
%ul.content-list.manage-labels-list.js-other-labels
- if @labels.present?
- = render partial: 'shared/label', collection: @labels, as: :label
+ = render partial: 'shared/label', subject: @project, collection: @labels, as: :label
= paginate @labels, theme: 'gitlab'
- if @labels.blank?
.nothing-here-block
diff --git a/app/views/projects/pipelines_settings/show.html.haml b/app/views/projects/pipelines_settings/show.html.haml
index bebf0ccd54d..96221a20502 100644
--- a/app/views/projects/pipelines_settings/show.html.haml
+++ b/app/views/projects/pipelines_settings/show.html.haml
@@ -5,33 +5,59 @@
%h4.prepend-top-0
= page_title
.col-lg-9
- %h5.prepend-top-0
- Pipelines
= form_for @project, url: namespace_project_pipelines_settings_path(@project.namespace.becomes(Namespace), @project) do |f|
%fieldset.builds-feature
- unless @repository.gitlab_ci_yml
.form-group
%p Pipelines need to be configured before you can begin using Continuous Integration.
= link_to 'Get started with CI/CD Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
+ %hr
+ .form-group.append-bottom-default
+ = f.label :runners_token, "Runner token", class: 'label-light'
+ = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
+ %p.help-block The secure token used by the Runner to checkout the project
+
+ %hr
.form-group
- %p Get recent application code using the following command:
+ %h5.prepend-top-0
+ Git strategy for pipelines
+ %p
+ Choose between <code>clone</code> or <code>fetch</code> to get the recent application code
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy')
.radio
= f.label :build_allow_git_fetch_false do
= f.radio_button :build_allow_git_fetch, 'false'
%strong git clone
%br
- %span.descr Slower but makes sure you have a clean dir before every build
+ %span.descr
+ Slower but makes sure the project workspace is pristine as it clones the repository from scratch for every job
.radio
= f.label :build_allow_git_fetch_true do
= f.radio_button :build_allow_git_fetch, 'true'
%strong git fetch
%br
- %span.descr Faster
+ %span.descr
+ Faster as it re-uses the project workspace (falling back to clone if it doesn't exist)
+ %hr
.form-group
= f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
- %p.help-block per build in minutes
+ %p.help-block
+ Per job in minutes. If a job passes this threshold, it will be marked as failed.
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout')
+
+ %hr
+ .form-group
+ .checkbox
+ = f.label :public_builds do
+ = f.check_box :public_builds
+ %strong Public pipelines
+ .help-block
+ Allow everyone to access pipelines for public and internal projects
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'visibility-of-pipelines')
+
+ %hr
.form-group
= f.label :build_coverage_regex, "Test coverage parsing", class: 'label-light'
.input-group
@@ -39,8 +65,9 @@
= f.text_field :build_coverage_regex, class: 'form-control', placeholder: '\(\d+.\d+\%\) covered'
%span.input-group-addon /
%p.help-block
- We will use this regular expression to find test coverage output in build trace.
- Leave blank if you want to disable this feature
+ A regular expression that will be used to find the test coverage
+ output in the build trace. Leave blank to disable
+ = link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'test-coverage-parsing')
.bs-callout.bs-callout-info
%p Below are examples of regex for existing tools:
%ul
@@ -57,21 +84,9 @@
gcovr (C/C++) -
%code ^TOTAL.*\s+(\d+\%)$
%li
- tap --coverage-report=text-summary (Node.js) -
+ tap --coverage-report=text-summary (NodeJS) -
%code ^Statements\s*:\s*([^%]+)
- .form-group
- .checkbox
- = f.label :public_builds do
- = f.check_box :public_builds
- %strong Public builds
- .help-block Allow everyone to access builds traces for Public and Internal projects
-
- .form-group.append-bottom-default
- = f.label :runners_token, "Runners token", class: 'label-light'
- = f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
- %p.help-block The secure token used to checkout project.
-
= f.submit 'Save changes', class: "btn btn-save"
%hr
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 05fccb4f976..c42641afea0 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -1,4 +1,4 @@
-- commit = @repository.commit(tag.target)
+- commit = @repository.commit(tag.dereferenced_target)
- release = @releases.find { |release| release.tag == tag.name }
%li
%div
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 3480800369a..c367ae336db 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,6 +1,9 @@
%ul.nav-links.event-filter.scrolling-tabs
= event_filter_link EventFilter.all, 'All'
- = event_filter_link EventFilter.push, 'Push events'
- = event_filter_link EventFilter.merged, 'Merge events'
- = event_filter_link EventFilter.comments, 'Comments'
+ - if event_filter_visible(:repository)
+ = event_filter_link EventFilter.push, 'Push events'
+ - if event_filter_visible(:merge_requests)
+ = event_filter_link EventFilter.merged, 'Merge events'
+ - if event_filter_visible(:issues)
+ = event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team'
diff --git a/app/views/shared/_label.html.haml b/app/views/shared/_label.html.haml
index 40c8d2af226..6ccdef0df46 100644
--- a/app/views/shared/_label.html.haml
+++ b/app/views/shared/_label.html.haml
@@ -1,6 +1,7 @@
- label_css_id = dom_id(label)
-- open_issues_count = label.open_issues_count(current_user, @project)
-- open_merge_requests_count = label.open_merge_requests_count(current_user, @project)
+- open_issues_count = label.open_issues_count(current_user)
+- open_merge_requests_count = label.open_merge_requests_count(current_user)
+- subject = local_assigns[:subject]
%li{id: label_css_id, data: { id: label.id } }
= render "shared/label_row", label: label
@@ -12,10 +13,10 @@
.dropdown-menu.dropdown-menu-align-right
%ul
%li
- = link_to_label(label, subject: @project, type: :merge_request) do
+ = link_to_label(label, subject: subject, type: :merge_request) do
= pluralize open_merge_requests_count, 'merge request'
%li
- = link_to_label(label, subject: @project) do
+ = link_to_label(label, subject: subject) do
= pluralize open_issues_count, 'open issue'
- if current_user
%li.label-subscription{ data: toggle_subscription_data(label) }
@@ -28,9 +29,9 @@
= link_to 'Delete', destroy_label_path(label), title: 'Delete', method: :delete, remote: true, data: {confirm: 'Remove this label? Are you sure?'}
.pull-right.hidden-xs.hidden-sm.hidden-md
- = link_to_label(label, subject: @project, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
+ = link_to_label(label, subject: subject, type: :merge_request, css_class: 'btn btn-transparent btn-action') do
= pluralize open_merge_requests_count, 'merge request'
- = link_to_label(label, subject: @project, css_class: 'btn btn-transparent btn-action') do
+ = link_to_label(label, subject: subject, css_class: 'btn btn-transparent btn-action') do
= pluralize open_issues_count, 'open issue'
- if current_user
diff --git a/app/views/shared/empty_states/_todos_all_done.svg b/app/views/shared/empty_states/_todos_all_done.svg
new file mode 100644
index 00000000000..94b5c2e0ea0
--- /dev/null
+++ b/app/views/shared/empty_states/_todos_all_done.svg
@@ -0,0 +1 @@
+<svg viewBox="0 0 293 216"><g fill="none" fill-rule="evenodd"><g transform="rotate(-5 211.388 -693.89)"><rect width="163.6" height="200" x=".2" fill="#FFF" stroke="#EEE" stroke-width="3" stroke-linecap="round" stroke-dasharray="6 9" rx="6"/><g transform="translate(24 38)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/></g><g transform="translate(24 83)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g><g transform="translate(24 130)"><path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/><path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/><rect width="76" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/><rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/></g></g><path fill="#FFCE29" d="M30 11l-1.8 4-2-4-4-1.8 4-2 2-4 2 4 4 2M286 60l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8M263 97l-2 4-2-4-4-2 4-2 2-4 2 4 4 2M12 85l-2.7 6.3-3-6-6-3 6-3 3-6 2.8 6.2 6.6 2.8"/></g></svg>
diff --git a/app/views/shared/empty_states/_todos_empty.svg b/app/views/shared/empty_states/_todos_empty.svg
new file mode 100644
index 00000000000..b1e661268fb
--- /dev/null
+++ b/app/views/shared/empty_states/_todos_empty.svg
@@ -0,0 +1,110 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 284 337" xmlns:xlink="http://www.w3.org/1999/xlink">
+ <defs>
+ <rect id="a" width="180" height="220" x="66.2" y="74.4" rx="6"/>
+ <mask id="l" width="180" height="220" x="0" y="0" fill="#fff">
+ <use xlink:href="#a"/>
+ </mask>
+ <rect id="b" width="180" height="220" rx="6"/>
+ <mask id="m" width="180" height="220" x="0" y="0" fill="#fff">
+ <use xlink:href="#b"/>
+ </mask>
+ <rect id="c" width="28" height="28" rx="4"/>
+ <mask id="n" width="28" height="28" x="0" y="0" fill="#fff">
+ <use xlink:href="#c"/>
+ </mask>
+ <rect id="d" width="28" height="28" rx="4"/>
+ <mask id="o" width="28" height="28" x="0" y="0" fill="#fff">
+ <use xlink:href="#d"/>
+ </mask>
+ <circle id="e" cx="21.5" cy="21.5" r="21.5"/>
+ <mask id="p" width="43" height="43" x="0" y="0" fill="#fff">
+ <use xlink:href="#e"/>
+ </mask>
+ <circle id="f" cx="26.5" cy="26.5" r="26.5"/>
+ <mask id="q" width="53" height="53" x="0" y="0" fill="#fff">
+ <use xlink:href="#f"/>
+ </mask>
+ <circle id="g" cx="9.5" cy="4.5" r="4.5"/>
+ <mask id="r" width="13" height="13" x="-2" y="-2">
+ <path fill="#fff" d="M3-2h13v13H3z"/>
+ <use xlink:href="#g"/>
+ </mask>
+ <circle id="h" cx="26.5" cy="26.5" r="26.5"/>
+ <mask id="s" width="53" height="53" x="0" y="0" fill="#fff">
+ <use xlink:href="#h"/>
+ </mask>
+ <circle id="i" cx="21.5" cy="21.5" r="21.5"/>
+ <mask id="t" width="43" height="43" x="0" y="0" fill="#fff">
+ <use xlink:href="#i"/>
+ </mask>
+ <path id="j" d="M18 38h15c10.5 0 19-8.5 19-19S43.5 0 33 0H19C8.5 0 0 8.5 0 19c0 6.3 3 12 7.8 15.3l5.2 9c.6 1 1.4 1 2 0l3-5.3z"/>
+ <mask id="u" width="52" height="44" x="0" y="0" fill="#fff">
+ <use xlink:href="#j"/>
+ </mask>
+ <circle id="k" cx="18.5" cy="18.5" r="18.5"/>
+ <mask id="v" width="37" height="37" x="0" y="0" fill="#fff">
+ <use xlink:href="#k"/>
+ </mask>
+ </defs>
+ <g fill="none" fill-rule="evenodd" transform="translate(-6 -4)">
+ <use stroke="#EEE" stroke-width="6" mask="url(#l)" transform="rotate(-5 156.245 184.425)" xlink:href="#a"/>
+ <g transform="rotate(5 -707.333 618.042)">
+ <use fill="#FFF" stroke="#EEE" stroke-width="6" mask="url(#m)" xlink:href="#b"/>
+ <g transform="translate(29 24)">
+ <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/>
+ <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/>
+ <rect width="86" height="3" x="40" y="11" fill="#6B4FBB" opacity=".5" rx="1.5"/>
+ <rect width="43" height="3" x="40" y="21" fill="#6B4FBB" opacity=".5" rx="1.5"/>
+ </g>
+ <g transform="translate(29 69)">
+ <path fill="#FC6D26" d="M18.2 14l-4-3.8c-.4-.6-1.4-.6-2 0-.6.6-.6 1.5 0 2l5 5c.3.4.6.5 1 .5s.8 0 1-.4L28 8.8c.6-.6.6-1.5 0-2-.6-.7-1.6-.7-2 0L18 14z"/>
+ <path stroke="#6B4FBB" stroke-width="3" d="M27 23.3V27c0 2.3-1.7 4-4 4H4c-2.3 0-4-1.7-4-4V8c0-2.3 1.7-4 4-4h3.8" stroke-linecap="round"/>
+ <rect width="86" height="3" x="40" y="11" fill="#B5A7DD" rx="1.5"/>
+ <rect width="43" height="3" x="40" y="21" fill="#B5A7DD" rx="1.5"/>
+ </g>
+ <g transform="translate(28 160)">
+ <use stroke="#E5E5E5" stroke-width="6" mask="url(#n)" opacity=".7" xlink:href="#c"/>
+ <rect width="26" height="3" x="41" y="7" fill="#ECECEC" rx="1.5"/>
+ <rect width="43" height="3" x="41" y="17" fill="#ECECEC" rx="1.5"/>
+ </g>
+ <g transform="translate(28 116)">
+ <use stroke="#E5E5E5" stroke-width="6" mask="url(#o)" xlink:href="#d"/>
+ <rect width="86" height="3" x="41" y="7" fill="#E5E5E5" rx="1.5"/>
+ <rect width="43" height="3" x="41" y="17" fill="#E5E5E5" rx="1.5"/>
+ </g>
+ </g>
+ <g transform="rotate(-15 601.917 -782.362)">
+ <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#p)" xlink:href="#e"/>
+ <text fill="#6B4FBB" font-family="SourceSansPro-Black, Source Sans Pro" font-size="20" font-weight="700" letter-spacing="-.1">
+ <tspan x="12" y="27">@</tspan>
+ </text>
+ </g>
+ <g transform="rotate(15 -686.59 1035.907)">
+ <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#q)" xlink:href="#f"/>
+ <path fill="#FC6D26" d="M26.5 38.2c3.3 0 9.5-2.5 9.5-9.6 0-7-2.4-6.6-9.5-6.6-7 0-9.5-.4-9.5 6.6s6.2 9.6 9.5 9.6z"/>
+ <g transform="translate(17 14)">
+ <use fill="#FC6D26" xlink:href="#g"/>
+ <use stroke="#FFF" stroke-width="4" mask="url(#r)" xlink:href="#g"/>
+ </g>
+ </g>
+ <g transform="rotate(15 -85.125 65.185)">
+ <use fill="#FFF" stroke="#B5A7DD" stroke-width="6" mask="url(#s)" xlink:href="#h"/>
+ <path fill="#6B4FBB" d="M24 18.5c0-1.4 1-2.5 2.5-2.5 1.4 0 2.5 1 2.5 2.5v9c0 1.4-1 2.5-2.5 2.5-1.4 0-2.5-1-2.5-2.5v-9zM26.5 37c1.4 0 2.5-1 2.5-2.5 0-1.4-1-2.5-2.5-2.5-1.4 0-2.5 1-2.5 2.5 0 1.4 1 2.5 2.5 2.5z"/>
+ </g>
+ <g transform="rotate(-15 716.492 78.873)">
+ <use fill="#FFF" stroke="#FDE5D8" stroke-width="6" mask="url(#t)" xlink:href="#i"/>
+ <path fill="#FC6D26" d="M20 23v-3h3v3h-3zm0 3v1.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-2.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-3h-1.5c-.8 0-1.5-.7-1.5-1.5s.7-1.5 1.5-1.5H17v-2.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h3v-1.5c0-.8.7-1.5 1.5-1.5s1.5.7 1.5 1.5V17h2.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v3h1.5c.8 0 1.5.7 1.5 1.5s-.7 1.5-1.5 1.5H26v2.5c0 .8-.7 1.5-1.5 1.5s-1.5-.7-1.5-1.5V26h-3z"/>
+ </g>
+ <g transform="rotate(-15 129.114 -585.74)">
+ <use stroke="#FDE5D8" stroke-width="6" mask="url(#u)" xlink:href="#j"/>
+ <circle cx="16" cy="20" r="2" fill="#FC6D26"/>
+ <circle cx="27" cy="20" r="2" fill="#FC6D26"/>
+ <circle cx="38" cy="20" r="2" fill="#FC6D26"/>
+ </g>
+ <g transform="rotate(-15 1254.8 -458.986)">
+ <use stroke="#FDE5D8" stroke-width="6" mask="url(#v)" xlink:href="#k"/>
+ <path fill="#FC6D26" d="M10.6 19l2-2c.5-.5.5-1 0-1.5-.3-.4-1-.4-1.3 0l-2.8 2.8c-.2.2-.3.4-.3.7 0 .3 0 .5.3.7l2.8 2.8c.4.4 1 .4 1.4 0 .4-.4.4-1 0-1.4l-2-2zm14.8 0l-2-2c-.5-.5-.5-1 0-1.5.3-.4 1-.4 1.3 0l2.8 2.8c.2.2.3.4.3.7 0 .3 0 .5-.3.7l-2.8 2.8c-.4.4-1 .4-1.4 0-.4-.4-.4-1 0-1.4l2-2z"/>
+ <rect width="2" height="7" x="17" y="15.1" fill="#FC6D26" opacity=".5" transform="rotate(15 18.002 18.64)" rx="1"/>
+ </g>
+ </g>
+</svg>
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index dc4ee3074d2..562291a61df 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -24,7 +24,8 @@
%span.visibility-icon.has-tooltip{data: { container: 'body', placement: 'left' }, title: visibility_icon_description(group)}
= visibility_level_icon(group.visibility_level, fw: false)
- = image_tag group_icon(group), class: "avatar s40 hidden-xs"
+ .image-container.s40
+ = image_tag group_icon(group), class: "avatar s40 hidden-xs"
.title
= link_to group, class: 'group-name' do
= group.name
diff --git a/app/views/shared/icons/_icon_close.svg b/app/views/shared/icons/_icon_close.svg
new file mode 100644
index 00000000000..9d62012518b
--- /dev/null
+++ b/app/views/shared/icons/_icon_close.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 15 15"><path d="M9,7.5l5.83-5.91a.48.48,0,0,0,0-.69L14.11.15a.46.46,0,0,0-.68,0l-5.93,6L1.57.15a.46.46,0,0,0-.68,0L.15.9a.48.48,0,0,0,0,.69L6,7.5.15,13.41a.48.48,0,0,0,0,.69l.74.75a.46.46,0,0,0,.68,0l5.93-6,5.93,6a.46.46,0,0,0,.68,0l.74-.75a.48.48,0,0,0,0-.69Z"/></svg> \ No newline at end of file
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 3c03c220ddd..9e1b0379428 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -3,8 +3,9 @@
- assignee = issuable.assignee
- issuable_type = issuable.class.table_name
- base_url_args = [project.namespace.becomes(Namespace), project, issuable_type]
+- can_update = can?(current_user, :"update_#{issuable.to_ability_name}", issuable)
-%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
+%li{ id: dom_id(issuable, 'sortable'), class: "issuable-row #{'ui-sort-disabled' unless can_update}", 'data-iid' => issuable.iid, 'data-url' => polymorphic_path(issuable) }
%span
- if show_project_name
%strong #{project.name} &middot;
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index e8668048703..3d2122a159c 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -32,10 +32,11 @@
= link_to project_path(project), class: dom_class(project) do
- if avatar
.dash-project-avatar
- - if use_creator_avatar
- = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
- - else
- = project_icon(project, alt: '', class: 'avatar project-avatar s40')
+ .image-container.s40
+ - if use_creator_avatar
+ = image_tag avatar_icon(project.creator.email, 40), class: "avatar s40", alt:''
+ - else
+ = project_icon(project, alt: '', class: 'avatar project-avatar s40')
%span.project-full-name
%span.namespace-name
- if project.namespace && !skip_namespace
diff --git a/app/views/users/_groups.html.haml b/app/views/users/_groups.html.haml
index f360fbb3d5d..78f253f9023 100644
--- a/app/views/users/_groups.html.haml
+++ b/app/views/users/_groups.html.haml
@@ -1,4 +1,5 @@
.clearfix
- groups.each do |group|
= link_to group, class: 'profile-groups-avatars inline', title: group.name do
- = image_tag group_icon(group), class: 'avatar group-avatar s40'
+ .image-container.s40
+ = image_tag group_icon(group), class: 'avatar group-avatar s40'
diff --git a/app/workers/project_cache_worker.rb b/app/workers/project_cache_worker.rb
index 71b274e0c99..4dfa745fb50 100644
--- a/app/workers/project_cache_worker.rb
+++ b/app/workers/project_cache_worker.rb
@@ -9,6 +9,18 @@ class ProjectCacheWorker
LEASE_TIMEOUT = 15.minutes.to_i
+ def self.lease_for(project_id)
+ Gitlab::ExclusiveLease.
+ new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT)
+ end
+
+ # Overwrite Sidekiq's implementation so we only schedule when actually needed.
+ def self.perform_async(project_id)
+ # If a lease for this project is still being held there's no point in
+ # scheduling a new job.
+ super unless lease_for(project_id).exists?
+ end
+
def perform(project_id)
if try_obtain_lease_for(project_id)
Rails.logger.
@@ -37,8 +49,6 @@ class ProjectCacheWorker
end
def try_obtain_lease_for(project_id)
- Gitlab::ExclusiveLease.
- new("project_cache_worker:#{project_id}", timeout: LEASE_TIMEOUT).
- try_obtain
+ self.class.lease_for(project_id).try_obtain
end
end
diff --git a/app/workers/project_web_hook_worker.rb b/app/workers/project_web_hook_worker.rb
index efb85eafd15..d973e662ff2 100644
--- a/app/workers/project_web_hook_worker.rb
+++ b/app/workers/project_web_hook_worker.rb
@@ -2,6 +2,8 @@ class ProjectWebHookWorker
include Sidekiq::Worker
include DedicatedSidekiqQueue
+ sidekiq_options retry: 4
+
def perform(hook_id, data, hook_name)
data = data.with_indifferent_access
WebHook.find(hook_id).execute(data, hook_name)
diff --git a/app/workers/remove_unreferenced_lfs_objects_worker.rb b/app/workers/remove_unreferenced_lfs_objects_worker.rb
new file mode 100644
index 00000000000..b80f131d5f7
--- /dev/null
+++ b/app/workers/remove_unreferenced_lfs_objects_worker.rb
@@ -0,0 +1,8 @@
+class RemoveUnreferencedLfsObjectsWorker
+ include Sidekiq::Worker
+ include CronjobQueue
+
+ def perform
+ LfsObject.destroy_unreferenced
+ end
+end
diff --git a/config/application.rb b/config/application.rb
index 92c8467e7f4..946b632b0e8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -91,6 +91,7 @@ module Gitlab
config.assets.precompile << "protected_branches/protected_branches_bundle.js"
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
+ config.assets.precompile << "cycle_analytics/cycle_analytics_bundle.js"
config.assets.precompile << "merge_conflicts/merge_conflicts_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
config.assets.precompile << "blob_edit/blob_edit_bundle.js"
diff --git a/config/dependency_decisions.yml b/config/dependency_decisions.yml
index 74325872b09..c11296975b7 100644
--- a/config/dependency_decisions.yml
+++ b/config/dependency_decisions.yml
@@ -101,6 +101,13 @@
:why: GPL-licensed libraries cannot be linked to from non-GPL projects.
:versions: []
:when: 2016-05-02 05:29:43.904715000 Z
+- - :blacklist
+ - OSL-3.0
+ - :who: Sean McGivern
+ :why: The OSL license is a copyleft license
+ :versions: []
+ :when: 2016-10-28 11:02:15.540105000 Z
+
# GEM LICENSES
- - :license
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 114ceac8e1f..3451b68cea5 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -547,6 +547,10 @@ test:
project_url: "http://redmine/projects/:issues_tracker_id"
issues_url: "http://redmine/:project_id/:issues_tracker_id/:id"
new_issue_url: "http://redmine/projects/:issues_tracker_id/issues/new"
+ jira:
+ title: "JIRA"
+ url: https://sample_company.atlasian.net
+ project_key: PROJECT
ldap:
enabled: false
servers:
diff --git a/config/initializers/0_post_deployment_migrations.rb b/config/initializers/0_post_deployment_migrations.rb
new file mode 100644
index 00000000000..0068a03d214
--- /dev/null
+++ b/config/initializers/0_post_deployment_migrations.rb
@@ -0,0 +1,12 @@
+# Post deployment migrations are included by default. This file must be loaded
+# before other initializers as Rails may otherwise memoize a list of migrations
+# excluding the post deployment migrations.
+unless ENV['SKIP_POST_DEPLOYMENT_MIGRATIONS']
+ path = Rails.root.join('db', 'post_migrate').to_s
+
+ Rails.application.config.paths['db/migrate'] << path
+
+ # Rails memoizes migrations at certain points where it won't read the above
+ # path just yet. As such we must also update the following list of paths.
+ ActiveRecord::Migrator.migrations_paths << path
+end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index efe0ac9c965..9fec2ad6bf7 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -307,6 +307,9 @@ Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWork
Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *'
Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker'
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['cron'] ||= '20 0 * * *'
+Settings.cron_jobs['remove_unreferenced_lfs_objects_worker']['job_class'] = 'RemoveUnreferencedLfsObjectsWorker'
#
# GitLab Shell
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index f7e714cd6bc..0455a98dbfe 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -42,3 +42,19 @@ end
Sidekiq.configure_client do |config|
config.redis = redis_config_hash
end
+
+# The Sidekiq client API always adds the queue to the Sidekiq queue
+# list, but mail_room and gitlab-shell do not. This is only necessary
+# for monitoring.
+config = YAML.load_file(Rails.root.join('config', 'sidekiq_queues.yml').to_s)
+
+begin
+ Sidekiq.redis do |conn|
+ conn.pipelined do
+ config[:queues].each do |queue|
+ conn.sadd('queues', queue[0])
+ end
+ end
+ end
+rescue Redis::BaseError, SocketError
+end
diff --git a/config/mail_room.yml b/config/mail_room.yml
index 68697bd1dc4..b026d510f1b 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -27,10 +27,25 @@
:namespace: <%= Gitlab::Redis::SIDEKIQ_NAMESPACE %>
:queue: email_receiver
:worker: EmailReceiverWorker
+ <% if config[:sentinels] %>
+ :sentinels:
+ <% config[:sentinels].each do |sentinel| %>
+ -
+ :host: <%= sentinel[:host] %>
+ :port: <%= sentinel[:port] %>
+ <% end %>
+ <% end %>
:arbitration_method: redis
:arbitration_options:
:redis_url: <%= config[:redis_url].to_json %>
:namespace: <%= Gitlab::Redis::MAILROOM_NAMESPACE %>
-
+ <% if config[:sentinels] %>
+ :sentinels:
+ <% config[:sentinels].each do |sentinel| %>
+ -
+ :host: <%= sentinel[:host] %>
+ :port: <%= sentinel[:port] %>
+ <% end %>
+ <% end %>
<% end %>
diff --git a/config/routes/group.rb b/config/routes/group.rb
index 4838c9d91c6..826048ba196 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -12,23 +12,26 @@ constraints(GroupUrlConstrainer.new) do
end
end
-resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
- member do
- get :issues
- get :merge_requests
- get :projects
- get :activity
- end
-
- scope module: :groups do
- resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
- post :resend_invite, on: :member
- delete :leave, on: :collection
+scope constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
+ resources :groups, except: [:show] do
+ member do
+ get :issues
+ get :merge_requests
+ get :projects
+ get :activity
end
- resource :avatar, only: [:destroy]
- resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
+ scope module: :groups do
+ resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
+ post :resend_invite, on: :member
+ delete :leave, on: :collection
+ end
- resources :labels, except: [:show], constraints: { id: /\d+/ }
+ resource :avatar, only: [:destroy]
+ resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
+
+ resources :labels, except: [:show], constraints: { id: /\d+/ }
+ end
end
+ get 'groups/:id' => 'groups#show', as: :group_canonical
end
diff --git a/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb b/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb
new file mode 100644
index 00000000000..319d86ac159
--- /dev/null
+++ b/db/migrate/20161011222551_remove_inactive_jira_service_properties.rb
@@ -0,0 +1,10 @@
+class RemoveInactiveJiraServiceProperties < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = true
+ DOWNTIME_REASON = "Removes all inactive jira_service properties"
+
+ def up
+ execute("UPDATE services SET properties = '{}' WHERE services.type = 'JiraService' and services.active = false")
+ end
+end
diff --git a/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb b/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb
new file mode 100644
index 00000000000..b47f3aa2810
--- /dev/null
+++ b/db/migrate/20161021114307_add_lock_version_to_build_and_pipelines.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLockVersionToBuildAndPipelines < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ def change
+ add_column :ci_builds, :lock_version, :integer
+ add_column :ci_commits, :lock_version, :integer
+ end
+end
diff --git a/db/migrate/20161025231710_migrate_jira_to_gem.rb b/db/migrate/20161025231710_migrate_jira_to_gem.rb
new file mode 100644
index 00000000000..870b00411d2
--- /dev/null
+++ b/db/migrate/20161025231710_migrate_jira_to_gem.rb
@@ -0,0 +1,73 @@
+class MigrateJiraToGem < ActiveRecord::Migration
+ DOWNTIME = true
+
+ DOWNTIME_REASON = <<-HEREDOC
+ Refactor all Jira services properties(serialized field) to use new jira-ruby gem.
+ There were properties on old Jira service that are not needed anymore after the
+ service refactoring: api_url, project_url, new_issue_url, issues_url.
+ We extract the new necessary some properties from old keys and delete them:
+ taking project_key from project_url and url from api_url
+ HEREDOC
+
+ def up
+ active_services_query = "SELECT id, properties FROM services WHERE services.type IN ('JiraService') AND services.active = true"
+
+ select_all(active_services_query).each do |service|
+ id = service['id']
+ properties = JSON.parse(service['properties'])
+ properties_was = properties.clone
+
+ # Migrate `project_url` to `project_key`
+ # Ignore if `project_url` doesn't have jql project query with project key
+ if properties['project_url'].present?
+ jql = properties['project_url'].match('project=([A-Za-z]*)')
+ properties['project_key'] = jql.captures.first if jql
+ end
+
+ # Migrate `api_url` to `url`
+ if properties['api_url'].present?
+ url = properties['api_url'].match('(.*)\/rest\/api')
+ properties['url'] = url.captures.first if url
+ end
+
+ # Delete now unnecessary properties
+ properties.delete('api_url')
+ properties.delete('project_url')
+ properties.delete('new_issue_url')
+ properties.delete('issues_url')
+
+ # Update changes properties
+ if properties != properties_was
+ execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}")
+ end
+ end
+ end
+
+ def down
+ active_services_query = "SELECT id, properties FROM services WHERE services.type IN ('JiraService') AND services.active = true"
+
+ select_all(active_services_query).each do |service|
+ id = service['id']
+ properties = JSON.parse(service['properties'])
+ properties_was = properties.clone
+
+ # Rebuild old properties based on sane defaults
+ if properties['url'].present?
+ properties['api_url'] = "#{properties['url']}/rest/api/2"
+ properties['project_url'] =
+ "#{properties['url']}/issues/?jql=project=#{properties['project_key']}"
+ properties['issues_url'] = "#{properties['url']}/browse/:id"
+ properties['new_issue_url'] = "#{properties['url']}/secure/CreateIssue.jspa"
+ end
+
+ # Delete the new properties
+ properties.delete('url')
+ properties.delete('project_key')
+
+ # Update changes properties
+ if properties != properties_was
+ execute("UPDATE services SET properties = '#{quote_string(properties.to_json)}' WHERE id = #{id}")
+ end
+ end
+ end
+end
diff --git a/db/post_migrate/.gitkeep b/db/post_migrate/.gitkeep
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/db/post_migrate/.gitkeep
diff --git a/db/schema.rb b/db/schema.rb
index 02282b0f666..54b5fc83be0 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: 20161024042317) do
+ActiveRecord::Schema.define(version: 20161025231710) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -189,6 +189,7 @@ ActiveRecord::Schema.define(version: 20161024042317) do
t.text "yaml_variables"
t.datetime "queued_at"
t.string "token"
+ t.integer "lock_version"
end
add_index "ci_builds", ["commit_id", "stage_idx", "created_at"], name: "index_ci_builds_on_commit_id_and_stage_idx_and_created_at", using: :btree
@@ -219,6 +220,7 @@ ActiveRecord::Schema.define(version: 20161024042317) do
t.datetime "finished_at"
t.integer "duration"
t.integer "user_id"
+ t.integer "lock_version"
end
add_index "ci_commits", ["gl_project_id", "sha"], name: "index_ci_commits_on_gl_project_id_and_sha", using: :btree
diff --git a/doc/api/README.md b/doc/api/README.md
index 3fbe5197a21..f65b934b9db 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -23,6 +23,7 @@ following locations:
- [Group Access Requests](access_requests.md)
- [Group Members](members.md)
- [Issues](issues.md)
+- [Issue Boards](boards.md)
- [Keys](keys.md)
- [Labels](labels.md)
- [Merge Requests](merge_requests.md)
diff --git a/doc/api/deploy_keys.md b/doc/api/deploy_keys.md
index ca44afbf355..5f248ab6f91 100644
--- a/doc/api/deploy_keys.md
+++ b/doc/api/deploy_keys.md
@@ -2,7 +2,7 @@
## List all deploy keys
-Get a list of all deploy keys across all projects.
+Get a list of all deploy keys across all projects of the GitLab instance. This endpoint requires admin access.
```
GET /deploy_keys
diff --git a/doc/api/projects.md b/doc/api/projects.md
index b69db90e70d..8ebac57e612 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -598,7 +598,7 @@ Parameters:
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
@@ -630,7 +630,7 @@ Parameters:
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
@@ -661,7 +661,7 @@ Parameters:
| `container_registry_enabled` | boolean | no | Enable container registry for this project |
| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
-| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `visibility_level` | integer | no | See [project visibility level](#project-visibility-level) |
| `import_url` | string | no | URL to import repository from |
| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
diff --git a/doc/api/services.md b/doc/api/services.md
index 579fdc0c8c9..c7f537aceb6 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -451,43 +451,49 @@ GET /projects/:id/services/irker
## JIRA
-Jira issue tracker
+JIRA issue tracker.
+
+### Get JIRA service settings
+
+Get JIRA service settings for a project.
+
+```
+GET /projects/:id/services/jira
+```
### Create/Edit JIRA service
Set JIRA service for a project.
-> Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to easily navigate to the Jira issue tracker. See the [integration doc](http://docs.gitlab.com/ce/integration/external-issue-tracker.html) for details. Support for referencing commits and automatic closing of Jira issues directly from GitLab is [available in GitLab EE.](http://docs.gitlab.com/ee/integration/jira.html)
+>**Note:**
+Setting `project_url`, `issues_url` and `new_issue_url` will allow a user to
+easily navigate to the JIRA issue tracker. See the [integration doc][jira-doc]
+for details.
```
PUT /projects/:id/services/jira
```
-Parameters:
-
-- `new_issue_url` (**required**) - New Issue url
-- `project_url` (**required**) - Project url
-- `issues_url` (**required**) - Issue url
-- `description` (optional) - Jira issue tracker
-- `username` (optional) - Jira username
-- `password` (optional) - Jira password
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `active` | boolean| no | Enable/disable the JIRA service. |
+| `project_url` | string | yes | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. |
+| `issues_url` | string | yes | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime.|
+| `new_issue_url` | string | yes | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` |
+| `api_url` | string | yes | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. |
+| `description` | string | no | A name for the issue tracker. |
+| `username` | string | no | The username of the user created to be used with GitLab/JIRA. |
+| `password` | string | no | The password of the user created to be used with GitLab/JIRA. |
+| `jira_issue_transition_id` | string | no | The ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
### Delete JIRA service
-Delete JIRA service for a project.
+Remove all previously JIRA settings from a project.
```
DELETE /projects/:id/services/jira
```
-### Get JIRA service settings
-
-Get JIRA service settings for a project.
-
-```
-GET /projects/:id/services/jira
-```
-
## PivotalTracker
Project Management Software (Source Commits Endpoint)
@@ -662,3 +668,5 @@ Get JetBrains TeamCity CI service settings for a project.
```
GET /projects/:id/services/teamcity
```
+
+[jira-doc]: ../project_services/jira.md
diff --git a/doc/api/tags.md b/doc/api/tags.md
index 54059117456..398b080e3f6 100644
--- a/doc/api/tags.md
+++ b/doc/api/tags.md
@@ -124,7 +124,7 @@ Parameters:
The message will be `nil` when creating a lightweight tag otherwise
it will contain the annotation.
-It returns 200 if the operation succeed. In case of an error,
+It returns 201 if the operation succeed. In case of an error,
405 with an explaining error message is returned.
## Delete a tag
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 341bc85a16a..6b90940c047 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -19,4 +19,5 @@
- [Build permissions](../user/permissions.md#build-permissions)
- [API](../api/ci/README.md)
- [CI services (linked docker containers)](services/README.md)
+- [CI/CD pipelines settings](../user/project/pipelines/settings.md)
- [**New CI build permissions model**](../user/project/new_ci_build_permissions_model.md) Read about what changed in GitLab 8.12 and how that affects your builds. There's a new way to access your Git submodules and LFS objects in builds.
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 79bbe8421c6..a313c31e7ee 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -242,10 +242,10 @@ docker-in-docker on your runners, this is how your `.gitlab-ci.yml` could look:
- docker push registry.example.com/group/project:latest
```
-You have to use the credentials of the special `gitlab-ci-token` user with its
-password stored in `$CI_BUILD_TOKEN` in order to push to the Registry connected
-to your project. This allows you to automate building and deployment of your
-Docker images.
+You have to use the special `gitlab-ci-token` user created for you in order to
+push to the Registry connected to your project. Its password is provided in the
+`$CI_BUILD_TOKEN` variable. This allows you to automate building and deployment
+of your Docker images.
Here's a more elaborate example that splits up the tasks into 4 pipeline stages,
including two tests that run in parallel. The build is stored in the container
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index 729c1dc8c0d..7d100a4fd93 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -5,9 +5,9 @@ Introduced in GitLab 8.8.
## Pipelines
-A pipeline is a group of [builds] that get executed in [stages] \(batches). All
-of the builds in a stage are executed in parallel (if there are enough
-concurrent [runners]), and if they all succeed, the pipeline moves on to the
+A pipeline is a group of [builds][] that get executed in [stages][](batches).
+All of the builds in a stage are executed in parallel (if there are enough
+concurrent [Runners]), and if they all succeed, the pipeline moves on to the
next stage. If one of the builds fails, the next stage is not (usually)
executed.
@@ -25,8 +25,8 @@ See full [documentation](yaml/README.md#jobs).
## Seeing pipeline status
-You can find the current and historical pipeline runs under **Pipelines** for your
-project.
+You can find the current and historical pipeline runs under **Pipelines** for
+your project.
## Seeing build status
@@ -36,42 +36,11 @@ cancel the build, retry it, or erase the build trace.
## Badges
-There are build status and test coverage report badges available.
-
-Go to pipeline settings to see available badges and code you can use to embed
-badges in the `README.md` or your website.
-
-### Build status badge
-
-You can access a build status badge image using following link:
-
-```
-http://example.gitlab.com/namespace/project/badges/branch/build.svg
-```
-
-### Test coverage report badge
-
-GitLab makes it possible to define the regular expression for coverage report,
-that each build log will be matched against. This means that each build in the
-pipeline can have the test coverage percentage value defined.
-
-You can access test coverage badge using following link:
-
-```
-http://example.gitlab.com/namespace/project/badges/branch/coverage.svg
-```
-
-If you would like to get the coverage report from the specific job, you can add
-a `job=coverage_job_name` parameter to the URL. For example, it is possible to
-use following Markdown code to embed the est coverage report into `README.md`:
-
-```markdown
-![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
-```
-
-The latest successful pipeline will be used to read the test coverage value.
+Build status and test coverage report badges are available. You can find their
+respective link in the [Pipelines settings] page.
[builds]: #builds
[jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
-[runners]: runners/README.md
+[runners]: runners/READM
+[pipelines settings]: ../user/project/pipelines/settings.md
diff --git a/doc/development/README.md b/doc/development/README.md
index fb6a8a5b095..3f2151bbe8e 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -8,6 +8,8 @@
## Styleguides
+- [API styleguide](api_styleguide.md) Use this styleguide if you are
+ contributing to the API.
- [Documentation styleguide](doc_styleguide.md) Use this styleguide if you are
contributing to documentation.
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
@@ -39,6 +41,7 @@
- [What requires downtime?](what_requires_downtime.md)
- [Adding database indexes](adding_database_indexes.md)
+- [Post Deployment Migrations](post_deployment_migrations.md)
## Compliance
diff --git a/doc/development/api_styleguide.md b/doc/development/api_styleguide.md
new file mode 100644
index 00000000000..ce444ebdde4
--- /dev/null
+++ b/doc/development/api_styleguide.md
@@ -0,0 +1,96 @@
+# API styleguide
+
+This styleguide recommends best practices for API development.
+
+## Instance variables
+
+Please do not use instance variables, there is no need for them (we don't need
+to access them as we do in Rails views), local variables are fine.
+
+## Entities
+
+Always use an [Entity] to present the endpoint's payload.
+
+## Methods and parameters description
+
+Every method must be described using the [Grape DSL](https://github.com/ruby-grape/grape#describing-methods)
+(see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb
+for a good example):
+
+- `desc` for the method summary. You should pass it a block for additional
+ details such as:
+ - The GitLab version when the endpoint was added
+ - If the endpoint is deprecated, and if so, when will it be removed
+
+- `params` for the method params. This acts as description,
+ [validation, and coercion of the parameters]
+
+A good example is as follows:
+
+```ruby
+desc 'Get all broadcast messages' do
+ detail 'This feature was introduced in GitLab 8.12.'
+ success Entities::BroadcastMessage
+end
+params do
+ optional :page, type: Integer, desc: 'Current page number'
+ optional :per_page, type: Integer, desc: 'Number of messages per page'
+end
+get do
+ messages = BroadcastMessage.all
+
+ present paginate(messages), with: Entities::BroadcastMessage
+end
+```
+
+## Declared params
+
+> Grape allows you to access only the parameters that have been declared by your
+`params` block. It filters out the params that have been passed, but are not
+allowed.
+
+– https://github.com/ruby-grape/grape#declared
+
+### Exclude params from parent namespaces!
+
+> By default `declared(params) `includes parameters that were defined in all
+parent namespaces.
+
+– https://github.com/ruby-grape/grape#include-parent-namespaces
+
+In most cases you will want to exclude params from the parent namespaces:
+
+```ruby
+declared(params, include_parent_namespaces: false)
+```
+
+### When to use `declared(params)`?
+
+You should always use `declared(params)` when you pass the params hash as
+arguments to a method call.
+
+For instance:
+
+```ruby
+# bad
+User.create(params) # imagine the user submitted `admin=1`... :)
+
+# good
+User.create(declared(params, include_parent_namespaces: false).to_h)
+```
+
+>**Note:**
+`declared(params)` return a `Hashie::Mash` object, on which you will have to
+call `.to_h`.
+
+But we can use `params[key]` directly when we access single elements.
+
+For instance:
+
+```ruby
+# good
+Model.create(foo: params[:foo])
+```
+
+[Entity]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/entities.rb
+[validation, and coercion of the parameters]: https://github.com/ruby-grape/grape#parameter-validation-and-coercion
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index f07d2c9af2d..2cfa30f652e 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -93,6 +93,8 @@ merge request.
links shift too, which eventually leads to dead links. If you think it is
compelling to add numbers in headings, make sure to at least discuss it with
someone in the Merge Request
+- Avoid adding things that show ephemeral statuses. For example, if a feature is
+ considered beta or experimental, put this info in a note, not in the heading.
- When introducing a new document, be careful for the headings to be
grammatically and syntactically correct. It is advised to mention one or all
of the following GitLab members for a review: `@axil`, `@rspeicher`, `@marcia`,
@@ -342,12 +344,6 @@ You can use the following fake tokens as examples.
Here is a list of must-have items. Use them in the exact order that appears
on this document. Further explanation is given below.
-- Every method must be described using [Grape's DSL](https://github.com/ruby-grape/grape/tree/v0.13.0#describing-methods)
- (see https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/api/environments.rb
- for a good example):
- - `desc` for the method summary (you can pass it a block for additional details)
- - `params` for the method params (this acts as description **and** validation
- of the params)
- Every method must have the REST API request. For example:
```
@@ -472,4 +468,4 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "domain
[doc-restart]: ../administration/restart_gitlab.md "GitLab restart documentation"
[ce-3349]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3349 "Documentation restructure"
[graffle]: https://gitlab.com/gitlab-org/gitlab-design/blob/d8d39f4a87b90fb9ae89ca12dc565347b4900d5e/production/resources/gitlab-map.graffle
-[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png \ No newline at end of file
+[gitlab-map]: https://gitlab.com/gitlab-org/gitlab-design/raw/master/production/resources/gitlab-map.png
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
index 56c8516508e..ece8f880542 100644
--- a/doc/development/frontend.md
+++ b/doc/development/frontend.md
@@ -185,6 +185,20 @@ again in the future.
See [the Testing Standards and Style Guidelines](testing.md) for more
information.
+### Running frontend tests
+
+`rake teaspoon` runs the frontend-only (JavaScript) tests.
+It consists of two subtasks:
+
+- `rake teaspoon:fixtures` (re-)generates fixtures
+- `rake teaspoon:tests` actually executes the tests
+
+As long as the fixtures don't change, `rake teaspoon:tests` is sufficient
+(and saves you some time).
+
+Please note: Not all of the frontend fixtures are generated. Some are still static
+files. These will not be touched by `rake teaspoon:fixtures`.
+
## Supported browsers
For our currently-supported browsers, see our [requirements][requirements].
@@ -224,13 +238,18 @@ For our currently-supported browsers, see our [requirements][requirements].
[scss-style-guide]: scss_styleguide.md
[requirements]: ../install/requirements.md#supported-web-browsers
-## Common Errors
+## Gotchas
-### Rspec (Capybara/Poltergeist) chokes on general JavaScript errors
+### Phantom.JS (used by Teaspoon & Rspec) chokes, returning vague JavaScript errors
If you see very generic JavaScript errors (e.g. `jQuery is undefined`) being thrown in tests, but
can't reproduce them manually, you may have included `ES6`-style JavaScript in files that don't
have the `.js.es6` file extension. Either use ES5-friendly JavaScript or rename the file you're
-working in (`git mv <file>.js> <file.js.es6>`).
+working in (`git mv <file.js> <file.js.es6>`).
+
+Similar errors will be thrown if you're using
+any of the [array methods introduced in ES6](http://www.2ality.com/2014/05/es6-array-methods.html)
+whether or not you've updated the file extension.
+
diff --git a/doc/development/gotchas.md b/doc/development/gotchas.md
index 159d5ce286d..b25ce79e89f 100644
--- a/doc/development/gotchas.md
+++ b/doc/development/gotchas.md
@@ -41,9 +41,9 @@ Rubocop](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-4-stable/.rubocop.yml#L9
[Exception]: http://stackoverflow.com/q/10048173/223897
-## Don't use inline CoffeeScript/JavaScript in views
+## Don't use inline JavaScript in views
-Using the inline `:coffee` or `:coffeescript` Haml filters comes with a
+Using the inline `:javascript` Haml filters comes with a
performance overhead. Using inline JavaScript is not a good way to structure your code and should be avoided.
_**Note:** We've [removed these two filters](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/config/initializers/hamlit.rb)
@@ -51,9 +51,7 @@ in an initializer._
### Further reading
-- Pull Request: [Replace CoffeeScript block into JavaScript in Views](https://git.io/vztMu)
- Stack Overflow: [Why you should not write inline JavaScript](http://programmers.stackexchange.com/questions/86589/why-should-i-avoid-inline-scripting)
-- Stack Overflow: [Performance implications of using :coffescript filter inside HAML templates?](http://stackoverflow.com/a/17571242/223897)
## ID-based CSS selectors need to be a bit more specific
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 05972b33fdb..5d177eb26ee 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -62,6 +62,7 @@ Libraries with the following licenses are unacceptable for use:
- [GNU GPL][GPL] (version 1, [version 2][GPLv2], [version 3][GPLv3], or any future versions): GPL-licensed libraries cannot be linked to from non-GPL projects.
- [GNU AGPLv3][AGPLv3]: AGPL-licensed libraries cannot be linked to from non-GPL projects.
+- [Open Software License (OSL)][OSL]: is a copyleft license. In addition, the FSF [recommend against its use][OSL-GNU].
## Notes
@@ -93,3 +94,5 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[AGPLv3]: http://choosealicense.com/licenses/agpl-3.0/
[GNU-GPL-FAQ]: http://www.gnu.org/licenses/gpl-faq.html#IfLibraryIsGPL
[OSI-GPL]: https://opensource.org/faq#linking-proprietary-code
+[OSL]: https://opensource.org/licenses/OSL-3.0
+[OSL-GNU]: https://www.gnu.org/licenses/license-list.en.html#OSL
diff --git a/doc/development/post_deployment_migrations.md b/doc/development/post_deployment_migrations.md
new file mode 100644
index 00000000000..cfc91539bee
--- /dev/null
+++ b/doc/development/post_deployment_migrations.md
@@ -0,0 +1,75 @@
+# Post Deployment Migrations
+
+Post deployment migrations are regular Rails migrations that can optionally be
+executed after a deployment. By default these migrations are executed alongside
+the other migrations. To skip these migrations you will have to set the
+environment variable `SKIP_POST_DEPLOYMENT_MIGRATIONS` to a non-empty value
+when running `rake db:migrate`.
+
+For example, this would run all migrations including any post deployment
+migrations:
+
+```bash
+bundle exec rake db:migrate
+```
+
+This however will skip post deployment migrations:
+
+```bash
+SKIP_POST_DEPLOYMENT_MIGRATIONS=true bundle exec rake db:migrate
+```
+
+## Deployment Integration
+
+Say you're using Chef for deploying new versions of GitLab and you'd like to run
+post deployment migrations after deploying a new version. Let's assume you
+normally use the command `chef-client` to do so. To make use of this feature
+you'd have to run this command as follows:
+
+```bash
+SKIP_POST_DEPLOYMENT_MIGRATIONS=true sudo chef-client
+```
+
+Once all servers have been updated you can run `chef-client` again on a single
+server _without_ the environment variable.
+
+The process is similar for other deployment techniques: first you would deploy
+with the environment variable set, then you'll essentially re-deploy a single
+server but with the variable _unset_.
+
+## Creating Migrations
+
+To create a post deployment migration you can use the following Rails generator:
+
+```bash
+bundle exec rails g post_deployment_migration migration_name_here
+```
+
+This will generate the migration file in `db/post_migrate`. These migrations
+behave exactly like regular Rails migrations.
+
+## Use Cases
+
+Post deployment migrations can be used to perform migrations that mutate state
+that an existing version of GitLab depends on. For example, say you want to
+remove a column from a table. This requires downtime as a GitLab instance
+depends on this column being present while it's running. Normally you'd follow
+these steps in such a case:
+
+1. Stop the GitLab instance
+2. Run the migration removing the column
+3. Start the GitLab instance again
+
+Using post deployment migrations we can instead follow these steps:
+
+1. Deploy a new version of GitLab while ignoring post deployment migrations
+2. Re-run `rake db:migrate` but without the environment variable set
+
+Here we don't need any downtime as the migration takes place _after_ a new
+version (which doesn't depend on the column anymore) has been deployed.
+
+Some other examples where these migrations are useful:
+
+* Cleaning up data generated due to a bug in GitLab
+* Removing tables
+* Migrating jobs from one Sidekiq queue to another
diff --git a/doc/development/testing.md b/doc/development/testing.md
index 513457d203a..8e91ac5e3ba 100644
--- a/doc/development/testing.md
+++ b/doc/development/testing.md
@@ -36,8 +36,8 @@ the command line via `bundle exec teaspoon`, or via a web browser at
`http://localhost:3000/teaspoon` when the Rails server is running.
- JavaScript tests live in `spec/javascripts/`, matching the folder structure of
- `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.coffee` has a corresponding
- `spec/javascripts/behaviors/autosize_spec.js.coffee` file.
+ `app/assets/javascripts/`: `app/assets/javascripts/behaviors/autosize.js.es6` has a corresponding
+ `spec/javascripts/behaviors/autosize_spec.js.es6` file.
- Haml fixtures required for JavaScript tests live in
`spec/javascripts/fixtures`. They should contain the bare minimum amount of
markup necessary for the test.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index c9acc9cdfb0..795e1d23443 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -142,6 +142,9 @@ gitlab-workhorse we need a Go compiler. The instructions below assume you
use 64-bit Linux. You can find downloads for other platforms at the [Go download
page](https://golang.org/dl).
+ # Remove former Go installation folder
+ sudo rm -rf /usr/local/go
+
curl --remote-name --progress https://storage.googleapis.com/golang/go1.5.3.linux-amd64.tar.gz
echo '43afe0c5017e502630b1aea4d44b8a7f059bf60d7f29dfd58db454d4e4e0ae53 go1.5.3.linux-amd64.tar.gz' | shasum -a256 -c - && \
sudo tar -C /usr/local -xzf go1.5.3.linux-amd64.tar.gz
diff --git a/doc/integration/README.md b/doc/integration/README.md
index c2fd299db07..a928b74f9b8 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -5,7 +5,7 @@ trackers and external authentication.
See the documentation below for details on how to configure these services.
-- [Jira](../project_services/jira.md) Integrate with the JIRA issue tracker
+- [JIRA](jira.md) Integrate with the JIRA issue tracker
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
- [LDAP](ldap.md) Set up sign in via LDAP
- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd and Azure
diff --git a/doc/project_services/img/jira_add_user_to_group.png b/doc/integration/img/jira_add_user_to_group.png
index 0ba737bda9a..0ba737bda9a 100644
--- a/doc/project_services/img/jira_add_user_to_group.png
+++ b/doc/integration/img/jira_add_user_to_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group.png b/doc/integration/img/jira_create_new_group.png
index 0609060cb05..0609060cb05 100644
--- a/doc/project_services/img/jira_create_new_group.png
+++ b/doc/integration/img/jira_create_new_group.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_group_name.png b/doc/integration/img/jira_create_new_group_name.png
index 53d77b17df0..53d77b17df0 100644
--- a/doc/project_services/img/jira_create_new_group_name.png
+++ b/doc/integration/img/jira_create_new_group_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_create_new_user.png b/doc/integration/img/jira_create_new_user.png
index 9eaa444ed25..9eaa444ed25 100644
--- a/doc/project_services/img/jira_create_new_user.png
+++ b/doc/integration/img/jira_create_new_user.png
Binary files differ
diff --git a/doc/project_services/img/jira_group_access.png b/doc/integration/img/jira_group_access.png
index 8d4657427ae..8d4657427ae 100644
--- a/doc/project_services/img/jira_group_access.png
+++ b/doc/integration/img/jira_group_access.png
Binary files differ
diff --git a/doc/project_services/img/jira_issue_reference.png b/doc/integration/img/jira_issue_reference.png
index 1a2d9f04a6c..1a2d9f04a6c 100644
--- a/doc/project_services/img/jira_issue_reference.png
+++ b/doc/integration/img/jira_issue_reference.png
Binary files differ
diff --git a/doc/integration/img/jira_merge_request_close.png b/doc/integration/img/jira_merge_request_close.png
new file mode 100644
index 00000000000..b8f6058a514
--- /dev/null
+++ b/doc/integration/img/jira_merge_request_close.png
Binary files differ
diff --git a/doc/project_services/img/jira_project_name.png b/doc/integration/img/jira_project_name.png
index e785ec6140d..e785ec6140d 100644
--- a/doc/project_services/img/jira_project_name.png
+++ b/doc/integration/img/jira_project_name.png
Binary files differ
diff --git a/doc/project_services/img/jira_service.png b/doc/integration/img/jira_service.png
index 13aefce6f84..13aefce6f84 100644
--- a/doc/project_services/img/jira_service.png
+++ b/doc/integration/img/jira_service.png
Binary files differ
diff --git a/doc/project_services/img/jira_service_close_issue.png b/doc/integration/img/jira_service_close_issue.png
index eed69e80d2c..eed69e80d2c 100644
--- a/doc/project_services/img/jira_service_close_issue.png
+++ b/doc/integration/img/jira_service_close_issue.png
Binary files differ
diff --git a/doc/integration/img/jira_service_page.png b/doc/integration/img/jira_service_page.png
new file mode 100644
index 00000000000..0cc160bebe2
--- /dev/null
+++ b/doc/integration/img/jira_service_page.png
Binary files differ
diff --git a/doc/project_services/img/jira_user_management_link.png b/doc/integration/img/jira_user_management_link.png
index 5f002b59bac..5f002b59bac 100644
--- a/doc/project_services/img/jira_user_management_link.png
+++ b/doc/integration/img/jira_user_management_link.png
Binary files differ
diff --git a/doc/project_services/img/jira_workflow_screenshot.png b/doc/integration/img/jira_workflow_screenshot.png
index 937a50a77d9..937a50a77d9 100644
--- a/doc/project_services/img/jira_workflow_screenshot.png
+++ b/doc/integration/img/jira_workflow_screenshot.png
Binary files differ
diff --git a/doc/integration/jira.md b/doc/integration/jira.md
index 78aa6634116..cf1557ddc44 100644
--- a/doc/integration/jira.md
+++ b/doc/integration/jira.md
@@ -1,3 +1,197 @@
# GitLab JIRA integration
-This document was moved under [project_services/jira](../project_services/jira.md).
+GitLab can be configured to interact with JIRA. Configuration happens via
+user name and password. Connecting to a JIRA server via CAS is not possible.
+
+Each project can be configured to connect to a different JIRA instance, see the
+[configuration](#configuration) section. If you have one JIRA instance you can
+pre-fill the settings page with a default template. To configure the template
+see the [Services Templates][services-templates] document.
+
+Once the project is connected to JIRA, you can reference and close the issues
+in JIRA directly from GitLab.
+
+## Table of Contents
+* [Referencing JIRA Issues from GitLab](#referencing-JIRA-issues)
+* [Closing JIRA Issues from GitLab](#closing-JIRA-issues)
+* [Configuration](#configuration)
+
+### Referencing JIRA Issues
+
+When GitLab project has JIRA issue tracker configured and enabled, mentioning
+JIRA issue in GitLab will automatically add a comment in JIRA issue with the
+link back to GitLab. This means that in comments in merge requests and commits
+referencing an issue, eg. `PROJECT-7`, will add a comment in JIRA issue in the
+format:
+
+```
+ USER mentioned this issue in RESOURCE_NAME of [PROJECT_NAME|LINK_TO_COMMENT]:
+ ENTITY_TITLE
+```
+
+* `USER` A user that mentioned the issue. This is the link to the user profile in GitLab.
+* `LINK_TO_THE_COMMENT` Link to the origin of mention with a name of the entity where JIRA issue was mentioned.
+* `RESOURCE_NAME` Kind of resource which referenced the issue. Can be a commit or merge request.
+* `PROJECT_NAME` GitLab project name.
+* `ENTITY_TITLE` Merge request title or commit message first line.
+
+![example of mentioning or closing the JIRA issue](img/jira_issue_reference.png)
+
+---
+
+### Closing JIRA Issues
+
+JIRA issues can be closed directly from GitLab by using trigger words, eg.
+`Resolves PROJECT-1`, `Closes PROJECT-1` or `Fixes PROJECT-1`, in commits and
+merge requests. When a commit which contains the trigger word in the commit
+message is pushed, GitLab will add a comment in the mentioned JIRA issue.
+
+For example, for project named `PROJECT` in JIRA, we implemented a new feature
+and created a merge request in GitLab.
+
+This feature was requested in JIRA issue `PROJECT-7`. Merge request in GitLab
+contains the improvement and in merge request description we say that this
+merge request `Closes PROJECT-7` issue.
+
+Once this merge request is merged, the JIRA issue will be automatically closed
+with a link to the commit that resolved the issue.
+
+![A Git commit that causes the JIRA issue to be closed](img/jira_merge_request_close.png)
+
+---
+
+![The GitLab integration user leaves a comment on JIRA](img/jira_service_close_issue.png)
+
+---
+
+## Configuration
+
+### Configuring JIRA
+
+We need to create a user in JIRA which will have access to all projects that
+need to integrate with GitLab. Login to your JIRA instance as admin and under
+Administration go to User Management and create a new user.
+
+As an example, we'll create a user named `gitlab` and add it to `JIRA-developers`
+group.
+
+**It is important that the user `GitLab` has write-access to projects in JIRA**
+
+We have split this stage in steps so it is easier to follow.
+
+---
+
+1. Login to your JIRA instance as an administrator and under **Administration**
+ go to **User Management** to create a new user.
+
+ ![JIRA user management link](img/jira_user_management_link.png)
+
+ ---
+
+1. The next step is to create a new user (e.g., `gitlab`) who has write access
+ to projects in JIRA. Enter the user's name and a _valid_ e-mail address
+ since JIRA sends a verification e-mail to set-up the password.
+ _**Note:** JIRA creates the username automatically by using the e-mail
+ prefix. You can change it later if you want._
+
+ ![JIRA create new user](img/jira_create_new_user.png)
+
+ ---
+
+1. Now, let's create a `gitlab-developers` group which will have write access
+ to projects in JIRA. Go to the **Groups** tab and select **Create group**.
+
+ ![JIRA create new user](img/jira_create_new_group.png)
+
+ ---
+
+ Give it an optional description and hit **Create group**.
+
+ ![jira create new group](img/jira_create_new_group_name.png)
+
+ ---
+
+1. Give the newly-created group write access by going to
+ **Application access > View configuration** and adding the `gitlab-developers`
+ group to JIRA Core.
+
+ ![JIRA group access](img/jira_group_access.png)
+
+ ---
+
+1. Add the `gitlab` user to the `gitlab-developers` group by going to
+ **Users > GitLab user > Add group** and selecting the `gitlab-developers`
+ group from the dropdown menu. Notice that the group says _Access_ which is
+ what we aim for.
+
+ ![JIRA add user to group](img/jira_add_user_to_group.png)
+
+---
+
+The JIRA configuration is over. Write down the new JIRA username and its
+password as they will be needed when configuring GitLab in the next section.
+
+### Configuring GitLab
+
+JIRA configuration in GitLab is done via a project's **Services**.
+
+#### GitLab 13.0 with JIRA v1000.x
+
+To enable JIRA integration in a project, navigate to the project's
+and open the context menu clicking on the top right gear icon, then go to
+**Services > JIRA**.
+
+Fill in the required details on the page as described in the table below.
+
+| Field | Description |
+| ----- | ----------- |
+| `URL` | The base URL to the JIRA project which is being linked to this GitLab project. Ex. https://JIRA.example.com |
+| `Project key` | The short, all capital letter identifier for your JIRA project. |
+| `Username` | The user name created in [configuring JIRA step](#configuring-JIRA). |
+| `Password` |The password of the user created in [configuring JIRA step](#configuring-JIRA). |
+| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). |
+
+After saving the configuration, your GitLab project will be able to interact
+with the linked JIRA project.
+
+![JIRA service page](img/jira_service_page.png)
+
+---
+
+#### GitLab 6.x-7.7 with JIRA v6.x
+
+_**Note:** GitLab versions 13.0 and up contain various integration improvements.
+We strongly recommend upgrading._
+
+In `gitlab.yml` enable the JIRA issue tracker section by
+[uncommenting these lines][JIRA-gitlab-yml]. This will make sure that all
+issues within GitLab are pointing to the JIRA issue tracker.
+
+After you set this, you will be able to close issues in JIRA by a commit in
+GitLab.
+
+Go to your project's **Settings** page and fill in the project name for the
+JIRA project:
+
+![Set the JIRA project name in GitLab to 'NEW'](img/jira_project_name.png)
+
+---
+
+You can also enable the JIRA service that will allow you to interact with JIRA
+issues. Go to the **Settings > Services > JIRA** and:
+
+1. Tick the active check box to enable the service
+1. Supply the URL to JIRA server, for example http://JIRA.example.com
+1. Supply the username of a user we created under `Configuring JIRA` section,
+ for example `gitlab`
+1. Supply the password of the user
+1. Optional: supply the JIRA API version, default is version `2`
+1. Optional: supply the JIRA issue transition ID (issue transition to closed).
+ This is dependent on JIRA settings, default is `2`
+1. Hit save
+
+
+![JIRA services page](img/jira_service.png)
+
+[services-templates]: ../project_services/services_templates.md
+[JIRA-gitlab-yml]: https://gitlab.com/subscribers/gitlab-ee/blob/6-8-stable-ee/config/gitlab.yml.example#L111-115
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index f3b2a288776..4a242c321aa 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -268,13 +268,20 @@ message `Can't verify CSRF token authenticity`. This means that there is an erro
the SAML request, but this error never reaches GitLab due to the CSRF check.
To bypass this you can add `skip_before_action :verify_authenticity_token` to the
-`omniauth_callbacks_controller.rb` file. This will allow the error to hit GitLab,
-where it can then be seen in the usual logs, or as a flash message in the login
-screen.
-
-That file is located at `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
-for Omnibus installations and by default on `/home/git/gitlab/app/controllers` for
-installations from source.
+`omniauth_callbacks_controller.rb` file immediately after the `class` line and
+comment out the `protect_from_forgery` line using a `#` then restart Unicorn. This
+will allow the error to hit GitLab, where it can then be seen in the usual logs,
+or as a flash message on the login screen.
+
+That file is located in `/opt/gitlab/embedded/service/gitlab-rails/app/controllers`
+for Omnibus installations and by default in `/home/git/gitlab/app/controllers` for
+installations from source. Restart Unicorn using the `sudo gitlab-ctl restart unicorn`
+command on Omnibus installations and `sudo service gitlab restart` on installations
+from source.
+
+You may also find the [SSO Tracer](https://addons.mozilla.org/en-US/firefox/addon/sso-tracer)
+(Firefox) and [SAML Chrome Panel](https://chrome.google.com/webstore/detail/saml-chrome-panel/paijfdbeoenhembfhkhllainmocckace)
+(Chrome) browser extensions useful in your debugging.
### Invalid audience
diff --git a/doc/profile/two_factor_authentication.md b/doc/profile/two_factor_authentication.md
index 82505b13401..3f6dfe03d14 100644
--- a/doc/profile/two_factor_authentication.md
+++ b/doc/profile/two_factor_authentication.md
@@ -117,6 +117,22 @@ Click on **Authenticate via U2F Device** to complete the process.
This will clear all your two-factor authentication registrations, including mobile
applications and U2F devices.
+## Personal access tokens
+
+When 2FA is enabled, you can no longer use your normal account password to
+authenticate with Git over HTTPS on the command line, you must use a personal
+access token instead.
+
+1. Log in to your GitLab account.
+1. Go to your **Profile Settings**.
+1. Go to **Access Tokens**.
+1. Choose a name and expiry date for the token.
+1. Click on **Create Personal Access Token**.
+1. Save the personal access token somewhere safe.
+
+When using git over HTTPS on the command line, enter the personal access token
+into the password field.
+
## Note to GitLab administrators
You need to take special care to that 2FA keeps working after
diff --git a/doc/project_services/img/jira_add_gitlab_commit_message.png b/doc/project_services/img/jira_add_gitlab_commit_message.png
deleted file mode 100644
index aec472b9118..00000000000
--- a/doc/project_services/img/jira_add_gitlab_commit_message.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/jira_issue_closed.png b/doc/project_services/img/jira_issue_closed.png
deleted file mode 100644
index acdd83702d3..00000000000
--- a/doc/project_services/img/jira_issue_closed.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/jira_issues_workflow.png b/doc/project_services/img/jira_issues_workflow.png
deleted file mode 100644
index 0703081d77b..00000000000
--- a/doc/project_services/img/jira_issues_workflow.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/jira_merge_request_close.png b/doc/project_services/img/jira_merge_request_close.png
deleted file mode 100644
index 47785e3ba27..00000000000
--- a/doc/project_services/img/jira_merge_request_close.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png b/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png
deleted file mode 100644
index fb270d85e3c..00000000000
--- a/doc/project_services/img/jira_reference_commit_message_in_jira_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/jira_service_page.png b/doc/project_services/img/jira_service_page.png
deleted file mode 100644
index a5b49c501ba..00000000000
--- a/doc/project_services/img/jira_service_page.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/img/jira_submit_gitlab_merge_request.png b/doc/project_services/img/jira_submit_gitlab_merge_request.png
deleted file mode 100644
index 77630d39d39..00000000000
--- a/doc/project_services/img/jira_submit_gitlab_merge_request.png
+++ /dev/null
Binary files differ
diff --git a/doc/project_services/jira.md b/doc/project_services/jira.md
index b626c746c79..2ea1c58cb31 100644
--- a/doc/project_services/jira.md
+++ b/doc/project_services/jira.md
@@ -1,246 +1 @@
-# GitLab JIRA integration
-
->**Note:**
-Full JIRA integration was previously exclusive to GitLab Enterprise Edition.
-With [GitLab 8.3 forward][8_3_post], this feature in now [backported][jira-ce]
-to GitLab Community Edition as well.
-
----
-
-GitLab can be configured to interact with [JIRA Core] either using an
-on-premises instance or the SaaS solution that Atlassian offers. Configuration
-happens via username and password on a per-project basis. Connecting to a JIRA
-server via CAS is not possible.
-
-Each project can be configured to connect to a different JIRA instance or, in
-case you have a single JIRA instance, you can pre-fill the JIRA service
-settings page in GitLab with a default template. To configure the JIRA template,
-see the [Services Templates documentation][services-templates].
-
-Once the GitLab project is connected to JIRA, you can reference and close the
-issues in JIRA directly from GitLab's merge requests.
-
-## Configuration
-
-The configuration consists of two parts:
-
-- [JIRA configuration](#configuring-jira)
-- [GitLab configuration](#configuring-gitlab)
-
-### Configuring JIRA
-
-First things first, we need to create a user in JIRA which will have access to
-all projects that need to integrate with GitLab.
-
-We have split this stage in steps so it is easier to follow.
-
----
-
-1. Login to your JIRA instance as an administrator and under **Administration**
- go to **User Management** to create a new user.
-
- ![JIRA user management link](img/jira_user_management_link.png)
-
- ---
-
-1. The next step is to create a new user (e.g., `gitlab`) who has write access
- to projects in JIRA. Enter the user's name and a _valid_ e-mail address
- since JIRA sends a verification e-mail to set-up the password.
- _**Note:** JIRA creates the username automatically by using the e-mail
- prefix. You can change it later if you want._
-
- ![JIRA create new user](img/jira_create_new_user.png)
-
- ---
-
-1. Now, let's create a `gitlab-developers` group which will have write access
- to projects in JIRA. Go to the **Groups** tab and select **Create group**.
-
- ![JIRA create new user](img/jira_create_new_group.png)
-
- ---
-
- Give it an optional description and hit **Create group**.
-
- ![JIRA create new group](img/jira_create_new_group_name.png)
-
- ---
-
-1. Give the newly-created group write access by going to
- **Application access > View configuration** and adding the `gitlab-developers`
- group to JIRA Core.
-
- ![JIRA group access](img/jira_group_access.png)
-
- ---
-
-1. Add the `gitlab` user to the `gitlab-developers` group by going to
- **Users > GitLab user > Add group** and selecting the `gitlab-developers`
- group from the dropdown menu. Notice that the group says _Access_ which is
- what we aim for.
-
- ![JIRA add user to group](img/jira_add_user_to_group.png)
-
----
-
-The JIRA configuration is over. Write down the new JIRA username and its
-password as they will be needed when configuring GitLab in the next section.
-
-### Configuring GitLab
-
->**Note:**
-The currently supported JIRA versions are v6.x and v7.x. and GitLab
-7.8 or higher is required.
-
----
-
-Assuming you [have already configured JIRA](#configuring-jira), now it's time
-to configure GitLab.
-
-JIRA configuration in GitLab is done via a project's
-[**Services**](../project_services/project_services.md).
-
-To enable JIRA integration in a project, navigate to the project's
-**Settings > Services > JIRA**.
-
-Fill in the required details on the page, as described in the table below.
-
-| Setting | Description |
-| ------- | ----------- |
-| `Description` | A name for the issue tracker (to differentiate between instances, for example). |
-| `Project url` | The URL to the JIRA project which is being linked to this GitLab project. It is of the form: `https://<jira_host_url>/issues/?jql=project=<jira_project>`. |
-| `Issues url` | The URL to the JIRA project issues overview for the project that is linked to this GitLab project. It is of the form: `https://<jira_host_url>/browse/:id`. Leave `:id` as-is, it gets replaced by GitLab at runtime. |
-| `New issue url` | This is the URL to create a new issue in JIRA for the project linked to this GitLab project, and it is of the form: `https://<jira_host_url>/secure/CreateIssue.jspa` |
-| `Api url` | The base URL of the JIRA API. It may be omitted, in which case GitLab will automatically use API version `2` based on the `project url`. It is of the form: `https://<jira_host_url>/rest/api/2`. |
-| `Username` | The username of the user created in [configuring JIRA step](#configuring-jira). |
-| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
-| `JIRA issue transition` | This setting is very important to set up correctly. It is the ID of a transition that moves issues to a closed state. You can find this number under the JIRA workflow administration (**Administration > Issues > Workflows**) by selecting **View** under **Operations** of the desired workflow of your project. The ID of each state can be found inside the parenthesis of each transition name under the **Transitions (id)** column ([see screenshot][trans]). By default, this ID is set to `2`. |
-
-After saving the configuration, your GitLab project will be able to interact
-with the linked JIRA project.
-
-For example, given the settings below:
-
-- the JIRA URL is `https://jira.example.com`
-- the project is named `GITLAB`
-- the user is named `gitlab`
-- the JIRA issue transition is 151 (based on the [JIRA issue transition][trans])
-
-the following screenshot shows how the JIRA service settings should look like.
-
-![JIRA service page](img/jira_service_page.png)
-
-[trans]: img/jira_issues_workflow.png
-
----
-
-## JIRA issues
-
-By now you should have [configured JIRA](#configuring-jira) and enabled the
-[JIRA service in GitLab](#configuring-gitlab). If everything is set up correctly
-you should be able to reference and close JIRA issues by just mentioning their
-ID in GitLab commits and merge requests.
-
-### Referencing JIRA Issues
-
-If you reference a JIRA issue, e.g., `GITLAB-1`, in a commit comment, a link
-which points back to JIRA is created.
-
-The same works for comments in merge requests as well.
-
-![JIRA add GitLab commit message](img/jira_add_gitlab_commit_message.png)
-
----
-
-The mentioning action is two-fold, so a comment with a JIRA issue in GitLab
-will automatically add a comment in that particular JIRA issue with the link
-back to GitLab.
-
-
-![JIRA reference commit message](img/jira_reference_commit_message_in_jira_issue.png)
-
----
-
-The comment on the JIRA issue is of the form:
-
-> USER mentioned this issue in LINK_TO_THE_MENTION
-
-Where:
-
-| Format | Description |
-| ------ | ----------- |
-| `USER` | A user that mentioned the issue. This is the link to the user profile in GitLab. |
-| `LINK_TO_THE_MENTION` | Link to the origin of mention with a name of the entity where JIRA issue was mentioned. Can be commit or merge request. |
-
-### Closing JIRA issues
-
-JIRA issues can be closed directly from GitLab by using trigger words in
-commits and merge requests. When a commit which contains the trigger word
-followed by the JIRA issue ID in the commit message is pushed, GitLab will
-add a comment in the mentioned JIRA issue and immediately close it (provided
-the transition ID was set up correctly).
-
-There are currently three trigger words, and you can use either one to achieve
-the same goal:
-
-- `Resolves GITLAB-1`
-- `Closes GITLAB-1`
-- `Fixes GITLAB-1`
-
-where `GITLAB-1` the issue ID of the JIRA project.
-
-### JIRA issue closing example
-
-Let's say for example that we submitted a bug fix and created a merge request
-in GitLab. The workflow would be something like this:
-
-1. Create a new branch
-1. Fix the bug
-1. Commit the changes and push branch to GitLab
-1. Open a new merge request and reference the JIRA issue including one of the
- trigger words, e.g.: `Fixes GITLAB-1`, in the description
-1. Submit the merge request
-1. Ask someone to review
-1. Merge the merge request
-1. The JIRA issue is automatically closed
-
----
-
-In the following screenshot you can see what the link references to the JIRA
-issue look like.
-
-![JIRA - submit a GitLab merge request](img/jira_submit_gitlab_merge_request.png)
-
----
-
-Once this merge request is merged, the JIRA issue will be automatically closed
-with a link to the commit that resolved the issue.
-
-![The GitLab integration user leaves a comment on JIRA](img/jira_issue_closed.png)
-
----
-
-You can see from the above image that there are four references to GitLab:
-
-- The first is from a comment in a specific commit
-- The second is from the JIRA issue reference in the merge request description
-- The third is from the actual commit that solved the issue
-- And the fourth is from the commit that the merge request created
-
-[services-templates]: ../project_services/services_templates.md "Services templates documentation"
-[JIRA Core]: https://www.atlassian.com/software/jira/core "The JIRA Core website"
-[jira-ce]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2146 "MR - Backport JIRA service"
-[8_3_post]: https://about.gitlab.com/2015/12/22/gitlab-8-3-released/ "GitLab 8.3 release post"
-
-## Troubleshooting
-
-### GitLab is unable to comment on a ticket
-
-Make sure that the user you set up for GitLab to communicate with JIRA has the
-correct access permission to post comments on a ticket and to also transition the
-ticket, if you'd like GitLab to also take care of closing them.
-
-### GitLab is unable to close a ticket
-
-Make sure the the `Transition ID` you set within the JIRA settings matches the
-one your project needs to close a ticket.
+GitLab JIRA integration documentation has moved to [here](../integration/jira.md).
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index 4442b7c1742..8116a1ce976 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -40,7 +40,7 @@ further configuration instructions and details. Contributions are welcome.
| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
-| [JIRA](jira.md) | JIRA issue tracker |
+| [JIRA](../integration/jira.md) | JIRA issue tracker |
| JetBrains TeamCity CI | A continuous integration and build server |
| PivotalTracker | Project Management Software (Source Commits Endpoint) |
| Pushover | Pushover makes it easy to get real-time notifications on your Android device, iPhone, iPad, and Desktop |
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index fc0cd1b8af2..0ad84705cfd 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -85,8 +85,11 @@ Deleting old backups... [SKIPPING]
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
It uses the [Fog library](http://fog.io/) to perform the upload.
-In the example below we use Amazon S3 for storage.
-Fog also supports [other storage providers](http://fog.io/storage/).
+In the example below we use Amazon S3 for storage, but Fog also lets you use
+[other storage providers](http://fog.io/storage/). GitLab
+[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
+for AWS, Azure, Google, OpenStack Swift and Rackspace as well. A local driver is
+[also available](#uploading-to-locally-mounted-shares).
For omnibus packages:
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index 8940d14559b..c0084d9d59c 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -72,7 +72,7 @@ sudo -u git -H git checkout 8-13-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.6.3
+sudo -u git -H git checkout v3.6.6
```
### 6. Update gitlab-workhorse
diff --git a/doc/user/project/img/project_settings_list.png b/doc/user/project/img/project_settings_list.png
index 57ca2ac5f9e..cd9f5c00eea 100644
--- a/doc/user/project/img/project_settings_list.png
+++ b/doc/user/project/img/project_settings_list.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_settings_badges.png b/doc/user/project/pipelines/img/pipelines_settings_badges.png
new file mode 100644
index 00000000000..d0c4640791d
--- /dev/null
+++ b/doc/user/project/pipelines/img/pipelines_settings_badges.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
new file mode 100644
index 00000000000..d2a5568521f
--- /dev/null
+++ b/doc/user/project/pipelines/img/pipelines_settings_test_coverage.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_build.png b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png
new file mode 100644
index 00000000000..3823100daf2
--- /dev/null
+++ b/doc/user/project/pipelines/img/pipelines_test_coverage_build.png
Binary files differ
diff --git a/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
new file mode 100644
index 00000000000..c4f78803e69
--- /dev/null
+++ b/doc/user/project/pipelines/img/pipelines_test_coverage_mr_widget.png
Binary files differ
diff --git a/doc/user/project/pipelines/settings.md b/doc/user/project/pipelines/settings.md
new file mode 100644
index 00000000000..6cbcf3c400f
--- /dev/null
+++ b/doc/user/project/pipelines/settings.md
@@ -0,0 +1,113 @@
+# CI/CD pipelines settings
+
+To reach the pipelines settings:
+
+1. Navigate to your project and click the cog icon in the upper right corner.
+
+ ![Project settings menu](../img/project_settings_list.png)
+
+1. Select **CI/CD Pipelines** from the menu.
+
+The following settings can be configured per project.
+
+## Git strategy
+
+With Git strategy, you can choose the default way your repository is fetched
+from GitLab in a job.
+
+There are two options:
+
+- Using `git clone` which is slower since it clones the repository from scratch
+ for every job, ensuring that the project workspace is always pristine.
+- Using `git fetch` which is faster as it re-uses the project workspace (falling
+ back to clone if it doesn't exist).
+
+The default Git strategy can be overridden by the [GIT_STRATEGY variable][var]
+in `.gitlab-ci.yml`.
+
+## Timeout
+
+Timeout defines the maximum amount of time in minutes that a job is able run.
+The default value is 60 minutes. Decrease the time limit if you want to impose
+a hard limit on your jobs' running time or increase it otherwise. In any case,
+if the job surpasses the threshold, it is marked as failed.
+
+## Test coverage parsing
+
+If you use test coverage in your code, GitLab can capture its output in the
+build log using a regular expression. In the pipelines settings, search for the
+"Test coverage parsing" section.
+
+![Pipelines settings test coverage](img/pipelines_settings_test_coverage.png)
+
+Leave blank if you want to disable it or enter a ruby regular expression. You
+can use http://rubular.com to test your regex.
+
+If the pipeline succeeds, the coverage is shown in the merge request widget and
+in the builds table.
+
+![MR widget coverage](img/pipelines_test_coverage_mr_widget.png)
+
+![Build status coverage](img/pipelines_test_coverage_build.png)
+
+A few examples of known coverage tools for a variety of languages can be found
+in the pipelines settings page.
+
+## Visibility of pipelines
+
+For public and internal projects, the pipelines page can be accessed by
+anyone and those logged in respectively. If you wish to hide it so that only
+the members of the project or group have access to it, uncheck the **Public
+pipelines** checkbox and save the changes.
+
+## Badges
+
+In the pipelines settings page you can find build status and test coverage
+badges for your project. The latest successful pipeline will be used to read
+the build status and test coverage values.
+
+Visit the pipelines settings page in your project to see the exact link to
+your badges, as well as ways to embed the badge image in your HTML or Markdown
+pages.
+
+![Pipelines badges](img/pipelines_settings_badges.png)
+
+### Build status badge
+
+Depending on the status of your build, a badge can have the following values:
+
+- running
+- success
+- failed
+- skipped
+- unknown
+
+You can access a build status badge image using the following link:
+
+```
+https://example.gitlab.com/<namespace>/<project>/badges/<branch>/build.svg
+```
+
+### Test coverage report badge
+
+GitLab makes it possible to define the regular expression for [coverage report],
+that each build log will be matched against. This means that each build in the
+pipeline can have the test coverage percentage value defined.
+
+The test coverage badge can be accessed using following link:
+
+```
+https://example.gitlab.com/<namespace>/<project>/badges/<branch>/coverage.svg
+```
+
+If you would like to get the coverage report from a specific job, you can add
+the `job=coverage_job_name` parameter to the URL. For example, the following
+Markdown code will embed the test coverage report badge of the `coverage` job
+into your `README.md`:
+
+```markdown
+![coverage](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
+```
+
+[var]: ../../../ci/yaml/README.md#git-strategy
+[coverage report]: #test-coverage-parsing
diff --git a/features/dashboard/active_tab.feature b/features/dashboard/active_tab.feature
index 08b87808f33..bd883a0ebfa 100644
--- a/features/dashboard/active_tab.feature
+++ b/features/dashboard/active_tab.feature
@@ -18,7 +18,7 @@ Feature: Dashboard Active Tab
Then the active main tab should be Merge Requests
And no other main tabs should be active
- Scenario: On Dashboard Help
- Given I visit dashboard help page
- Then the active main tab should be Help
+ Scenario: On Dashboard Groups
+ Given I visit dashboard groups page
+ Then the active main tab should be Groups
And no other main tabs should be active
diff --git a/features/dashboard/dashboard.feature b/features/dashboard/dashboard.feature
index b1d5e4a7acb..92061dac7f4 100644
--- a/features/dashboard/dashboard.feature
+++ b/features/dashboard/dashboard.feature
@@ -11,7 +11,6 @@ Feature: Dashboard
And I visit dashboard page
Scenario: I should see projects list
- Then I should see "New Project" link
Then I should see "Shop" project link
Then I should see "Shop" project CI status
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index 244306e8464..007dfb67a77 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -163,7 +163,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see commit ci info' do
- expect(page).to have_content "Builds for 1 pipeline pending"
+ expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
end
step 'I click status link' do
@@ -171,7 +171,7 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
end
step 'I see builds list' do
- expect(page).to have_content "Builds for 1 pipeline pending"
+ expect(page).to have_content "Pipeline #1 for 570e7b2a pending"
expect(page).to have_content "1 build"
end
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 536199ddb4f..bd6466f3686 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -178,17 +178,17 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I fill jira settings' do
- fill_in 'Project url', with: 'http://jira.example'
+ fill_in 'URL', with: 'http://jira.example'
fill_in 'Username', with: 'gitlab'
fill_in 'Password', with: 'gitlab'
- fill_in 'Api url', with: 'http://jira.example/rest/api/2'
+ fill_in 'Project Key', with: 'GITLAB'
click_button 'Save'
end
step 'I should see jira service settings saved' do
- expect(find_field('Project url').value).to eq 'http://jira.example'
+ expect(find_field('URL').value).to eq 'http://jira.example'
expect(find_field('Username').value).to eq 'gitlab'
- expect(find_field('Api url').value).to eq 'http://jira.example/rest/api/2'
+ expect(find_field('Project Key').value).to eq 'GITLAB'
end
step 'I click Atlassian Bamboo CI service link' do
diff --git a/features/steps/shared/sidebar_active_tab.rb b/features/steps/shared/sidebar_active_tab.rb
index 5c47238777f..07fff16e867 100644
--- a/features/steps/shared/sidebar_active_tab.rb
+++ b/features/steps/shared/sidebar_active_tab.rb
@@ -1,12 +1,8 @@
module SharedSidebarActiveTab
include Spinach::DSL
- step 'the active main tab should be Help' do
- ensure_active_main_tab('Help')
- end
-
step 'no other main tabs should be active' do
- expect(page).to have_selector('.nav-sidebar > li.active', count: 1)
+ expect(page).to have_selector('.nav-sidebar li.active', count: 1)
end
def ensure_active_main_tab(content)
@@ -17,6 +13,10 @@ module SharedSidebarActiveTab
ensure_active_main_tab('Projects')
end
+ step 'the active main tab should be Groups' do
+ ensure_active_main_tab('Groups')
+ end
+
step 'the active main tab should be Projects' do
ensure_active_main_tab('Projects')
end
@@ -28,8 +28,4 @@ module SharedSidebarActiveTab
step 'the active main tab should be Merge Requests' do
ensure_active_main_tab('Merge Requests')
end
-
- step 'the active main tab should be Help' do
- ensure_active_main_tab('Help')
- end
end
diff --git a/generator_templates/rails/post_deployment_migration/migration.rb b/generator_templates/rails/post_deployment_migration/migration.rb
new file mode 100644
index 00000000000..1a7b8d5bf35
--- /dev/null
+++ b/generator_templates/rails/post_deployment_migration/migration.rb
@@ -0,0 +1,22 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class <%= migration_class_name %> < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ end
+end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 6d827448994..21a106387f0 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -6,58 +6,55 @@ module API
before { authenticate! }
before { authorize! :download_code, user_project }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get a project repository branches
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/repository/branches
+ desc 'Get a project repository branches' do
+ success Entities::RepoBranch
+ end
get ":id/repository/branches" do
branches = user_project.repository.branches.sort_by(&:name)
present branches, with: Entities::RepoBranch, project: user_project
end
- # Get a single branch
- #
- # Parameters:
- # id (required) - The ID of a project
- # branch (required) - The name of the branch
- # Example Request:
- # GET /projects/:id/repository/branches/:branch
- get ':id/repository/branches/:branch', requirements: { branch: /.+/ } do
- @branch = user_project.repository.branches.find { |item| item.name == params[:branch] }
- not_found!("Branch") unless @branch
+ desc 'Get a single branch' do
+ success Entities::RepoBranch
+ end
+ params do
+ requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ end
+ get ':id/repository/branches/:branch' do
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!("Branch") unless branch
- present @branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::RepoBranch, project: user_project
end
- # Protect a single branch
- #
# Note: The internal data model moved from `developers_can_{merge,push}` to `allowed_to_{merge,push}`
# in `gitlab-org/gitlab-ce!5081`. The API interface has not been changed (to maintain compatibility),
# but it works with the changed data model to infer `developers_can_merge` and `developers_can_push`.
- #
- # Parameters:
- # id (required) - The ID of a project
- # branch (required) - The name of the branch
- # developers_can_push (optional) - Flag if developers can push to that branch
- # developers_can_merge (optional) - Flag if developers can merge to that branch
- # Example Request:
- # PUT /projects/:id/repository/branches/:branch/protect
- put ':id/repository/branches/:branch/protect',
- requirements: { branch: /.+/ } do
+ desc 'Protect a single branch' do
+ success Entities::RepoBranch
+ end
+ params do
+ requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ optional :developers_can_push, type: Boolean, desc: 'Flag if developers can push to that branch'
+ optional :developers_can_merge, type: Boolean, desc: 'Flag if developers can merge to that branch'
+ end
+ put ':id/repository/branches/:branch/protect' do
authorize_admin_project
- @branch = user_project.repository.find_branch(params[:branch])
- not_found!('Branch') unless @branch
- protected_branch = user_project.protected_branches.find_by(name: @branch.name)
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!('Branch') unless branch
+
+ protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch_params = {
- name: @branch.name,
- developers_can_push: to_boolean(params[:developers_can_push]),
- developers_can_merge: to_boolean(params[:developers_can_merge])
+ name: branch.name,
+ developers_can_push: params[:developers_can_push],
+ developers_can_merge: params[:developers_can_merge]
}
service_args = [user_project, current_user, protected_branch_params]
@@ -69,39 +66,36 @@ module API
end
if protected_branch.valid?
- present @branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::RepoBranch, project: user_project
else
render_api_error!(protected_branch.errors.full_messages, 422)
end
end
- # Unprotect a single branch
- #
- # Parameters:
- # id (required) - The ID of a project
- # branch (required) - The name of the branch
- # Example Request:
- # PUT /projects/:id/repository/branches/:branch/unprotect
- put ':id/repository/branches/:branch/unprotect',
- requirements: { branch: /.+/ } do
+ desc 'Unprotect a single branch' do
+ success Entities::RepoBranch
+ end
+ params do
+ requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ end
+ put ':id/repository/branches/:branch/unprotect' do
authorize_admin_project
- @branch = user_project.repository.find_branch(params[:branch])
- not_found!("Branch") unless @branch
- protected_branch = user_project.protected_branches.find_by(name: @branch.name)
+ branch = user_project.repository.find_branch(params[:branch])
+ not_found!("Branch") unless branch
+ protected_branch = user_project.protected_branches.find_by(name: branch.name)
protected_branch.destroy if protected_branch
- present @branch, with: Entities::RepoBranch, project: user_project
+ present branch, with: Entities::RepoBranch, project: user_project
end
- # Create branch
- #
- # Parameters:
- # id (required) - The ID of a project
- # branch_name (required) - The name of the branch
- # ref (required) - Create branch from commit sha or existing branch
- # Example Request:
- # POST /projects/:id/repository/branches
+ desc 'Create branch' do
+ success Entities::RepoBranch
+ end
+ params do
+ requires :branch_name, type: String, desc: 'The name of the branch'
+ requires :ref, type: String, desc: 'Create branch from commit sha or existing branch'
+ end
post ":id/repository/branches" do
authorize_push_project
result = CreateBranchService.new(user_project, current_user).
@@ -116,16 +110,13 @@ module API
end
end
- # Delete branch
- #
- # Parameters:
- # id (required) - The ID of a project
- # branch (required) - The name of the branch
- # Example Request:
- # DELETE /projects/:id/repository/branches/:branch
- delete ":id/repository/branches/:branch",
- requirements: { branch: /.+/ } do
+ desc 'Delete a branch'
+ params do
+ requires :branch, type: String, regexp: /.+/, desc: 'The name of the branch'
+ end
+ delete ":id/repository/branches/:branch" do
authorize_push_project
+
result = DeleteBranchService.new(user_project, current_user).
execute(params[:branch])
diff --git a/lib/api/deploy_keys.rb b/lib/api/deploy_keys.rb
index 825e05fbae3..425df2c176a 100644
--- a/lib/api/deploy_keys.rb
+++ b/lib/api/deploy_keys.rb
@@ -49,18 +49,23 @@ module API
attrs = attributes_for_keys [:title, :key]
attrs[:key].strip! if attrs[:key]
+ # Check for an existing key joined to this project
key = user_project.deploy_keys.find_by(key: attrs[:key])
- present key, with: Entities::SSHKey if key
+ if key
+ present key, with: Entities::SSHKey
+ break
+ end
# Check for available deploy keys in other projects
key = current_user.accessible_deploy_keys.find_by(key: attrs[:key])
if key
user_project.deploy_keys << key
present key, with: Entities::SSHKey
+ break
end
+ # Create a new deploy key
key = DeployKey.new attrs
-
if key.valid? && user_project.deploy_keys << key
present key, with: Entities::SSHKey
else
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index feaa0c213bf..ab9d2d54f4b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -138,7 +138,7 @@ module API
expose :name
expose :commit do |repo_branch, options|
- options[:project].repository.commit(repo_branch.target)
+ options[:project].repository.commit(repo_branch.dereferenced_target)
end
expose :protected do |repo_branch, options|
@@ -523,7 +523,7 @@ module API
expose :name, :message
expose :commit do |repo_tag, options|
- options[:project].repository.commit(repo_tag.target)
+ options[:project].repository.commit(repo_tag.dereferenced_target)
end
expose :release, using: Entities::Release do |repo_tag, options|
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 45120898b76..3c9d7b1aaef 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -1,17 +1,12 @@
module API
module Helpers
+ include Gitlab::Utils
+
PRIVATE_TOKEN_HEADER = "HTTP_PRIVATE_TOKEN"
PRIVATE_TOKEN_PARAM = :private_token
SUDO_HEADER = "HTTP_SUDO"
SUDO_PARAM = :sudo
- def to_boolean(value)
- return true if value =~ /^(true|t|yes|y|1|on)$/i
- return false if value =~ /^(false|f|no|n|0|off)$/i
-
- nil
- end
-
def private_token
params[PRIVATE_TOKEN_PARAM] || env[PRIVATE_TOKEN_HEADER]
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 9a5d1ece070..ccf181402f9 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -17,15 +17,20 @@ module API
#
helpers do
+ def project_path
+ @project_path ||= begin
+ project_path = params[:project].sub(/\.git\z/, '')
+ Repository.remove_storage_from_path(project_path)
+ end
+ end
+
def wiki?
- @wiki ||= params[:project].end_with?('.wiki') &&
- !Project.find_with_namespace(params[:project])
+ @wiki ||= project_path.end_with?('.wiki') &&
+ !Project.find_with_namespace(project_path)
end
def project
@project ||= begin
- project_path = params[:project]
-
# Check for *.wiki repositories.
# Strip out the .wiki from the pathname before finding the
# project. This applies the correct project permissions to
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index 7b675e05fbb..bf2a199ce21 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -4,25 +4,24 @@ module API
before { authenticate! }
before { authorize! :download_code, user_project }
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
resource :projects do
- # Get a project repository tags
- #
- # Parameters:
- # id (required) - The ID of a project
- # Example Request:
- # GET /projects/:id/repository/tags
+ desc 'Get a project repository tags' do
+ success Entities::RepoTag
+ end
get ":id/repository/tags" do
present user_project.repository.tags.sort_by(&:name).reverse,
with: Entities::RepoTag, project: user_project
end
- # Get a single repository tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # Example Request:
- # GET /projects/:id/repository/tags/:tag_name
+ desc 'Get a single repository tag' do
+ success Entities::RepoTag
+ end
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ end
get ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
tag = user_project.repository.find_tag(params[:tag_name])
not_found!('Tag') unless tag
@@ -30,20 +29,21 @@ module API
present tag, with: Entities::RepoTag, project: user_project
end
- # Create tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # ref (required) - Create tag from commit sha or branch
- # message (optional) - Specifying a message creates an annotated tag.
- # Example Request:
- # POST /projects/:id/repository/tags
+ desc 'Create a new repository tag' do
+ success Entities::RepoTag
+ end
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ requires :ref, type: String, desc: 'The commit sha or branch name'
+ optional :message, type: String, desc: 'Specifying a message creates an annotated tag'
+ optional :release_description, type: String, desc: 'Specifying release notes stored in the GitLab database'
+ end
post ':id/repository/tags' do
authorize_push_project
- message = params[:message] || nil
+ create_params = declared(params)
+
result = CreateTagService.new(user_project, current_user).
- execute(params[:tag_name], params[:ref], message, params[:release_description])
+ execute(create_params[:tag_name], create_params[:ref], create_params[:message], create_params[:release_description])
if result[:status] == :success
present result[:tag],
@@ -54,15 +54,13 @@ module API
end
end
- # Delete tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # Example Request:
- # DELETE /projects/:id/repository/tags/:tag
+ desc 'Delete a repository tag'
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ end
delete ":id/repository/tags/:tag_name", requirements: { tag_name: /.+/ } do
authorize_push_project
+
result = DeleteTagService.new(user_project, current_user).
execute(params[:tag_name])
@@ -75,17 +73,16 @@ module API
end
end
- # Add release notes to tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # description (required) - Release notes with markdown support
- # Example Request:
- # POST /projects/:id/repository/tags/:tag_name/release
+ desc 'Add a release note to a tag' do
+ success Entities::Release
+ end
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ requires :description, type: String, desc: 'Release notes with markdown support'
+ end
post ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
authorize_push_project
- required_attributes! [:description]
+
result = CreateReleaseService.new(user_project, current_user).
execute(params[:tag_name], params[:description])
@@ -96,17 +93,16 @@ module API
end
end
- # Updates a release notes of a tag
- #
- # Parameters:
- # id (required) - The ID of a project
- # tag_name (required) - The name of the tag
- # description (required) - Release notes with markdown support
- # Example Request:
- # PUT /projects/:id/repository/tags/:tag_name/release
+ desc "Update a tag's release note" do
+ success Entities::Release
+ end
+ params do
+ requires :tag_name, type: String, desc: 'The name of the tag'
+ requires :description, type: String, desc: 'Release notes with markdown support'
+ end
put ':id/repository/tags/:tag_name/release', requirements: { tag_name: /.+/ } do
authorize_push_project
- required_attributes! [:description]
+
result = UpdateReleaseService.new(user_project, current_user).
execute(params[:tag_name], params[:description])
diff --git a/lib/api/users.rb b/lib/api/users.rb
index e868f628404..c28e07a76b7 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -333,11 +333,11 @@ module API
user = User.find_by(id: declared(params).id)
not_found!('User') unless user
- events = user.recent_events.
+ events = user.events.
merge(ProjectsFinder.new.execute(current_user)).
references(:project).
with_associations.
- page(params[:page])
+ recent
present paginate(events), with: Entities::Event
end
diff --git a/lib/backup/repository.rb b/lib/backup/repository.rb
index 9fcd9a3f999..d746070913d 100644
--- a/lib/backup/repository.rb
+++ b/lib/backup/repository.rb
@@ -2,11 +2,14 @@ require 'yaml'
module Backup
class Repository
+
def dump
prepare
Project.find_each(batch_size: 1000) do |project|
$progress.print " * #{project.path_with_namespace} ... "
+ path_to_project_repo = path_to_repo(project)
+ path_to_project_bundle = path_to_bundle(project)
# Create namespace dir if missing
FileUtils.mkdir_p(File.join(backup_repos_path, project.namespace.path)) if project.namespace
@@ -14,8 +17,22 @@ module Backup
if project.empty_repo?
$progress.puts "[SKIPPED]".color(:cyan)
else
- cmd = %W(tar -cf #{path_to_bundle(project)} -C #{path_to_repo(project)} .)
+ in_path(path_to_project_repo) do |dir|
+ FileUtils.mkdir_p(path_to_tars(project))
+ cmd = %W(tar -cf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
+ output, status = Gitlab::Popen.popen(cmd)
+
+ unless status.zero?
+ puts "[FAILED]".color(:red)
+ puts "failed: #{cmd.join(' ')}"
+ puts output
+ abort 'Backup failed'
+ end
+ end
+
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_project_repo} bundle create #{path_to_project_bundle} --all)
output, status = Gitlab::Popen.popen(cmd)
+
if status.zero?
$progress.puts "[DONE]".color(:green)
else
@@ -27,19 +44,22 @@ module Backup
end
wiki = ProjectWiki.new(project)
+ path_to_wiki_repo = path_to_repo(wiki)
+ path_to_wiki_bundle = path_to_bundle(wiki)
- if File.exist?(path_to_repo(wiki))
+ if File.exist?(path_to_wiki_repo)
$progress.print " * #{wiki.path_with_namespace} ... "
if wiki.repository.empty?
$progress.puts " [SKIPPED]".color(:cyan)
else
- cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_repo(wiki)} bundle create #{path_to_bundle(wiki)} --all)
+ cmd = %W(#{Gitlab.config.git.bin_path} --git-dir=#{path_to_wiki_repo} bundle create #{path_to_wiki_bundle} --all)
output, status = Gitlab::Popen.popen(cmd)
if status.zero?
$progress.puts " [DONE]".color(:green)
else
puts " [FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
+ puts output
abort 'Backup failed'
end
end
@@ -60,40 +80,59 @@ module Backup
Project.find_each(batch_size: 1000) do |project|
$progress.print " * #{project.path_with_namespace} ... "
+ path_to_project_repo = path_to_repo(project)
+ path_to_project_bundle = path_to_bundle(project)
project.ensure_dir_exist
- if File.exist?(path_to_bundle(project))
- FileUtils.mkdir_p(path_to_repo(project))
- cmd = %W(tar -xf #{path_to_bundle(project)} -C #{path_to_repo(project)})
+ if File.exists?(path_to_project_bundle)
+ cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_project_bundle} #{path_to_project_repo})
else
- cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_repo(project)})
+ cmd = %W(#{Gitlab.config.git.bin_path} init --bare #{path_to_project_repo})
end
- if system(*cmd, silent)
+ output, status = Gitlab::Popen.popen(cmd)
+ if status.zero?
$progress.puts "[DONE]".color(:green)
else
puts "[FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
+ puts output
abort 'Restore failed'
end
+ in_path(path_to_tars(project)) do |dir|
+ cmd = %W(tar -xf #{path_to_tars(project, dir)} -C #{path_to_project_repo} #{dir})
+
+ output, status = Gitlab::Popen.popen(cmd)
+ unless status.zero?
+ puts "[FAILED]".color(:red)
+ puts "failed: #{cmd.join(' ')}"
+ puts output
+ abort 'Restore failed'
+ end
+ end
+
wiki = ProjectWiki.new(project)
+ path_to_wiki_repo = path_to_repo(wiki)
+ path_to_wiki_bundle = path_to_bundle(wiki)
- if File.exist?(path_to_bundle(wiki))
+ if File.exist?(path_to_wiki_bundle)
$progress.print " * #{wiki.path_with_namespace} ... "
# If a wiki bundle exists, first remove the empty repo
# that was initialized with ProjectWiki.new() and then
# try to restore with 'git clone --bare'.
- FileUtils.rm_rf(path_to_repo(wiki))
- cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_bundle(wiki)} #{path_to_repo(wiki)})
+ FileUtils.rm_rf(path_to_wiki_repo)
+ cmd = %W(#{Gitlab.config.git.bin_path} clone --bare #{path_to_wiki_bundle} #{path_to_wiki_repo})
- if system(*cmd, silent)
+ output, status = Gitlab::Popen.popen(cmd)
+ if status.zero?
$progress.puts " [DONE]".color(:green)
else
puts " [FAILED]".color(:red)
puts "failed: #{cmd.join(' ')}"
+ puts output
abort 'Restore failed'
end
end
@@ -101,13 +140,15 @@ module Backup
$progress.print 'Put GitLab hooks in repositories dirs'.color(:yellow)
cmd = %W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args
- if system(*cmd)
+
+ output, status = Gitlab::Popen.popen(cmd)
+ if status.zero?
$progress.puts " [DONE]".color(:green)
else
puts " [FAILED]".color(:red)
puts "failed: #{cmd}"
+ puts output
end
-
end
protected
@@ -117,11 +158,30 @@ module Backup
end
def path_to_bundle(project)
- File.join(backup_repos_path, project.path_with_namespace + ".bundle")
+ File.join(backup_repos_path, project.path_with_namespace + '.bundle')
+ end
+
+ def path_to_tars(project, dir = nil)
+ path = File.join(backup_repos_path, project.path_with_namespace)
+
+ if dir
+ File.join(path, "#{dir}.tar")
+ else
+ path
+ end
end
def backup_repos_path
- File.join(Gitlab.config.backup.path, "repositories")
+ File.join(Gitlab.config.backup.path, 'repositories')
+ end
+
+ def in_path(path)
+ return unless Dir.exist?(path)
+
+ dir_entries = Dir.entries(path)
+ %w[annex custom_hooks].each do |entry|
+ yield(entry) if dir_entries.include?(entry)
+ end
end
def prepare
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index cb213a76a05..3740d4fb4cd 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -102,10 +102,10 @@ module Banzai
end
elsif element_node?(node)
- yield_valid_link(node) do |link, text|
+ yield_valid_link(node) do |link, inner_html|
if ref_pattern && link =~ /\A#{ref_pattern}\z/
replace_link_node_with_href(node, link) do
- object_link_filter(link, ref_pattern, link_text: text)
+ object_link_filter(link, ref_pattern, link_content: inner_html)
end
next
@@ -113,9 +113,9 @@ module Banzai
next unless link_pattern
- if link == text && text =~ /\A#{link_pattern}/
+ if link == inner_html && inner_html =~ /\A#{link_pattern}/
replace_link_node_with_text(node, link) do
- object_link_filter(text, link_pattern)
+ object_link_filter(inner_html, link_pattern)
end
next
@@ -123,7 +123,7 @@ module Banzai
if link =~ /\A#{link_pattern}\z/
replace_link_node_with_href(node, link) do
- object_link_filter(link, link_pattern, link_text: text)
+ object_link_filter(link, link_pattern, link_content: inner_html)
end
next
@@ -140,11 +140,11 @@ module Banzai
#
# text - String text to replace references in.
# pattern - Reference pattern to match against.
- # link_text - Original content of the link being replaced.
+ # link_content - Original content of the link being replaced.
#
# Returns a String with references replaced with links. All links
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
- def object_link_filter(text, pattern, link_text: nil)
+ def object_link_filter(text, pattern, link_content: nil)
references_in(text, pattern) do |match, id, project_ref, matches|
project = project_from_ref_cached(project_ref)
@@ -152,7 +152,7 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
- data = data_attributes_for(link_text || match, project, object)
+ data = data_attributes_for(link_content || match, project, object)
if matches.names.include?("url") && matches[:url]
url = matches[:url]
@@ -160,11 +160,11 @@ module Banzai
url = url_for_object_cached(object, project)
end
- text = link_text || object_link_text(object, matches)
+ content = link_content || object_link_text(object, matches)
%(<a href="#{url}" #{data}
title="#{escape_once(title)}"
- class="#{klass}">#{escape_once(text)}</a>)
+ class="#{klass}">#{content}</a>)
else
match
end
diff --git a/lib/banzai/filter/external_issue_reference_filter.rb b/lib/banzai/filter/external_issue_reference_filter.rb
index 0d20be557a0..dce4de3ceaf 100644
--- a/lib/banzai/filter/external_issue_reference_filter.rb
+++ b/lib/banzai/filter/external_issue_reference_filter.rb
@@ -37,10 +37,10 @@ module Banzai
end
elsif element_node?(node)
- yield_valid_link(node) do |link, text|
+ yield_valid_link(node) do |link, inner_html|
if link =~ ref_start_pattern
replace_link_node_with_href(node, link) do
- issue_link_filter(link, link_text: text)
+ issue_link_filter(link, link_content: inner_html)
end
end
end
@@ -54,10 +54,11 @@ module Banzai
# issue's details page.
#
# text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
#
# Returns a String with `JIRA-123` references replaced with links. All
# links have `gfm` and `gfm-issue` class names attached for styling.
- def issue_link_filter(text, link_text: nil)
+ def issue_link_filter(text, link_content: nil)
project = context[:project]
self.class.references_in(text, issue_reference_pattern) do |match, id|
@@ -69,11 +70,11 @@ module Banzai
klass = reference_class(:issue)
data = data_attribute(project: project.id, external_issue: id)
- text = link_text || match
+ content = link_content || match
%(<a href="#{url}" #{data}
title="#{escape_once(title)}"
- class="#{klass}">#{escape_once(text)}</a>)
+ class="#{klass}">#{content}</a>)
end
end
diff --git a/lib/banzai/filter/label_reference_filter.rb b/lib/banzai/filter/label_reference_filter.rb
index c24831e68ee..9f9a96cdc65 100644
--- a/lib/banzai/filter/label_reference_filter.rb
+++ b/lib/banzai/filter/label_reference_filter.rb
@@ -39,7 +39,7 @@ module Banzai
end
def find_labels(project)
- LabelsFinder.new(nil, project_id: project.id).execute(authorized_only: false)
+ LabelsFinder.new(nil, project_id: project.id).execute(skip_authorization: true)
end
# Parameters to pass to `Label.find_by` based on the given arguments
diff --git a/lib/banzai/filter/reference_filter.rb b/lib/banzai/filter/reference_filter.rb
index 2d221290f7e..84bfeac8041 100644
--- a/lib/banzai/filter/reference_filter.rb
+++ b/lib/banzai/filter/reference_filter.rb
@@ -85,14 +85,14 @@ module Banzai
@nodes ||= each_node.to_a
end
- # Yields the link's URL and text whenever the node is a valid <a> tag.
+ # Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
def yield_valid_link(node)
link = CGI.unescape(node.attr('href').to_s)
- text = node.text
+ inner_html = node.inner_html
return unless link.force_encoding('UTF-8').valid_encoding?
- yield link, text
+ yield link, inner_html
end
def replace_text_when_pattern_matches(node, pattern)
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index c6302b586d3..f842b1fb779 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -35,10 +35,10 @@ module Banzai
user_link_filter(content)
end
elsif element_node?(node)
- yield_valid_link(node) do |link, text|
+ yield_valid_link(node) do |link, inner_html|
if link =~ ref_pattern_start
replace_link_node_with_href(node, link) do
- user_link_filter(link, link_text: text)
+ user_link_filter(link, link_content: inner_html)
end
end
end
@@ -52,15 +52,16 @@ module Banzai
# user's profile page.
#
# text - String text to replace references in.
+ # link_content - Original content of the link being replaced.
#
# Returns a String with `@user` references replaced with links. All links
# have `gfm` and `gfm-project_member` class names attached for styling.
- def user_link_filter(text, link_text: nil)
+ def user_link_filter(text, link_content: nil)
self.class.references_in(text) do |match, username|
if username == 'all'
- link_to_all(link_text: link_text)
+ link_to_all(link_content: link_content)
elsif namespace = namespaces[username]
- link_to_namespace(namespace, link_text: link_text) || match
+ link_to_namespace(namespace, link_content: link_content) || match
else
match
end
@@ -102,49 +103,49 @@ module Banzai
reference_class(:project_member)
end
- def link_to_all(link_text: nil)
+ def link_to_all(link_content: nil)
project = context[:project]
author = context[:author]
if author && !project.team.member?(author)
- link_text
+ link_content
else
url = urls.namespace_project_url(project.namespace, project,
only_path: context[:only_path])
data = data_attribute(project: project.id, author: author.try(:id))
- text = link_text || User.reference_prefix + 'all'
+ content = link_content || User.reference_prefix + 'all'
- link_tag(url, data, text, 'All Project and Group Members')
+ link_tag(url, data, content, 'All Project and Group Members')
end
end
- def link_to_namespace(namespace, link_text: nil)
+ def link_to_namespace(namespace, link_content: nil)
if namespace.is_a?(Group)
- link_to_group(namespace.path, namespace, link_text: link_text)
+ link_to_group(namespace.path, namespace, link_content: link_content)
else
- link_to_user(namespace.path, namespace, link_text: link_text)
+ link_to_user(namespace.path, namespace, link_content: link_content)
end
end
- def link_to_group(group, namespace, link_text: nil)
+ def link_to_group(group, namespace, link_content: nil)
url = urls.group_url(group, only_path: context[:only_path])
data = data_attribute(group: namespace.id)
- text = link_text || Group.reference_prefix + group
+ content = link_content || Group.reference_prefix + group
- link_tag(url, data, text, namespace.name)
+ link_tag(url, data, content, namespace.name)
end
- def link_to_user(user, namespace, link_text: nil)
+ def link_to_user(user, namespace, link_content: nil)
url = urls.user_url(user, only_path: context[:only_path])
data = data_attribute(user: namespace.owner_id)
- text = link_text || User.reference_prefix + user
+ content = link_content || User.reference_prefix + user
- link_tag(url, data, text, namespace.owner_name)
+ link_tag(url, data, content, namespace.owner_name)
end
- def link_tag(url, data, text, title)
- %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{escape_once(text)}</a>)
+ def link_tag(url, data, link_content, title)
+ %(<a href="#{url}" #{data} class="#{link_class}" title="#{escape_once(title)}">#{link_content}</a>)
end
end
end
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index 0df3a72d1c4..de3ebe72720 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -41,10 +41,10 @@ module Banzai
next if visible.include?(node)
doc_data[:visible_reference_count] -= 1
- # The reference should be replaced by the original text,
- # which is not always the same as the rendered text.
- text = node.attr('data-original') || node.text
- node.replace(text)
+ # The reference should be replaced by the original link's content,
+ # which is not always the same as the rendered one.
+ content = node.attr('data-original') || node.inner_html
+ node.replace(content)
end
end
diff --git a/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
new file mode 100644
index 00000000000..7cb4bccb23c
--- /dev/null
+++ b/lib/generators/rails/post_deployment_migration/post_deployment_migration_generator.rb
@@ -0,0 +1,15 @@
+require 'rails/generators'
+
+module Rails
+ class PostDeploymentMigrationGenerator < Rails::Generators::NamedBase
+ def create_migration_file
+ timestamp = Time.now.strftime('%Y%m%d%H%I%S')
+
+ template "migration.rb", "db/post_migrate/#{timestamp}_#{file_name}.rb"
+ end
+
+ def migration_class_name
+ file_name.camelize
+ end
+ end
+end
diff --git a/lib/gitlab/data_builder/push.rb b/lib/gitlab/data_builder/push.rb
index 4f81863da35..d76aa38f741 100644
--- a/lib/gitlab/data_builder/push.rb
+++ b/lib/gitlab/data_builder/push.rb
@@ -83,7 +83,7 @@ module Gitlab
tag = repository.find_tag(tag_name)
if tag
- commit = repository.commit(tag.target)
+ commit = repository.commit(tag.dereferenced_target)
commit.try(:sha)
end
else
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index ffe49364379..7e8f35e9298 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -27,7 +27,7 @@ module Gitlab
# on begin/ensure blocks to cancel a lease, because the 'ensure' does
# not always run. Think of 'kill -9' from the Unicorn master for
# instance.
- #
+ #
# If you find that leases are getting in your way, ask yourself: would
# it be enough to lower the lease timeout? Another thing that might be
# appropriate is to only use a lease for bulk/automated operations, and
@@ -48,6 +48,13 @@ module Gitlab
end
end
+ # Returns true if the key for this lease is set.
+ def exists?
+ Gitlab::Redis.with do |redis|
+ redis.exists(redis_key)
+ end
+ end
+
# No #cancel method. See comments above!
private
diff --git a/lib/gitlab/fogbugz_import/importer.rb b/lib/gitlab/fogbugz_import/importer.rb
index 65ee85ca5a9..222bcdcbf9c 100644
--- a/lib/gitlab/fogbugz_import/importer.rb
+++ b/lib/gitlab/fogbugz_import/importer.rb
@@ -75,7 +75,7 @@ module Gitlab
def create_label(name)
params = { title: name, color: nice_label_color(name) }
- ::Labels::FindOrCreateService.new(project.owner, project, params).execute
+ ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
end
def user_info(person_id)
@@ -133,7 +133,7 @@ module Gitlab
updated_at: DateTime.parse(bug['dtLastUpdated'])
)
- issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute
+ issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
issue.update_attribute(:label_ids, issue_labels.pluck(:id))
import_issue_comments(issue, comments)
diff --git a/lib/gitlab/github_import/base_formatter.rb b/lib/gitlab/github_import/base_formatter.rb
index 8cacf4f4925..6dbae64a9fe 100644
--- a/lib/gitlab/github_import/base_formatter.rb
+++ b/lib/gitlab/github_import/base_formatter.rb
@@ -10,7 +10,9 @@ module Gitlab
end
def create!
- self.klass.create!(self.attributes)
+ project.public_send(project_association).find_or_create_by!(find_condition) do |record|
+ record.attributes = attributes
+ end
end
private
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 7f424b74efb..85df6547a67 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -105,18 +105,20 @@ module Gitlab
data = api.send(method, *args)
return data unless data.is_a?(Array)
+ last_response = api.last_response
+
if block_given?
yield data
- each_response_page(&block)
+ # api.last_response could change while we're yielding (e.g. fetching labels for each PR)
+ # so we cache our own last response
+ each_response_page(last_response, &block)
else
- each_response_page { |page| data.concat(page) }
+ each_response_page(last_response) { |page| data.concat(page) }
data
end
end
- def each_response_page
- last_response = api.last_response
-
+ def each_response_page(last_response)
while last_response.rels[:next]
sleep rate_limit_sleep_time if rate_limit_exceed?
last_response = last_response.rels[:next].get
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index 4b70f33a851..ecc28799737 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -24,7 +24,8 @@ module Gitlab
import_milestones
import_issues
import_pull_requests
- import_comments
+ import_comments(:issues)
+ import_comments(:pull_requests)
import_wiki
import_releases
handle_errors
@@ -48,7 +49,7 @@ module Gitlab
end
def import_labels
- client.labels(repo, per_page: 100) do |labels|
+ fetch_resources(:labels, repo, per_page: 100) do |labels|
labels.each do |raw|
begin
label = LabelFormatter.new(project, raw).create!
@@ -61,7 +62,7 @@ module Gitlab
end
def import_milestones
- client.milestones(repo, state: :all, per_page: 100) do |milestones|
+ fetch_resources(:milestones, repo, state: :all, per_page: 100) do |milestones|
milestones.each do |raw|
begin
MilestoneFormatter.new(project, raw).create!
@@ -73,7 +74,7 @@ module Gitlab
end
def import_issues
- client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
+ fetch_resources(:issues, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
issues.each do |raw|
gh_issue = IssueFormatter.new(project, raw)
@@ -90,7 +91,7 @@ module Gitlab
end
def import_pull_requests
- client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
+ fetch_resources(:pull_requests, repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
pull_requests.each do |raw|
pull_request = PullRequestFormatter.new(project, raw)
next unless pull_request.valid?
@@ -132,8 +133,15 @@ module Gitlab
end
def apply_labels(issuable, raw_issuable)
- if raw_issuable.labels.count > 0
- label_ids = raw_issuable.labels
+ # GH returns labels for issues but not for pull requests!
+ labels = if issuable.is_a?(MergeRequest)
+ client.labels_for_issue(repo, raw_issuable.number)
+ else
+ raw_issuable.labels
+ end
+
+ if labels.count > 0
+ label_ids = labels
.map { |attrs| @labels[attrs.name] }
.compact
@@ -141,23 +149,35 @@ module Gitlab
end
end
- def import_comments
- client.issues_comments(repo, per_page: 100) do |comments|
- create_comments(comments, :issue)
- end
+ def import_comments(issuable_type)
+ resource_type = "#{issuable_type}_comments".to_sym
+
+ # Two notes here:
+ # 1. We don't have a distinctive attribute for comments (unlike issues iid), so we fetch the last inserted note,
+ # compare it against every comment in the current imported page until we find match, and that's where start importing
+ # 2. GH returns comments for _both_ issues and PRs through issues_comments API, while pull_requests_comments returns
+ # only comments on diffs, so select last note not based on noteable_type but on line_code
+ line_code_is = issuable_type == :pull_requests ? 'NOT NULL' : 'NULL'
+ last_note = project.notes.where("line_code IS #{line_code_is}").last
+
+ fetch_resources(resource_type, repo, per_page: 100) do |comments|
+ if last_note
+ discard_inserted_comments(comments, last_note)
+ last_note = nil
+ end
- client.pull_requests_comments(repo, per_page: 100) do |comments|
- create_comments(comments, :pull_request)
+ create_comments(comments)
end
end
- def create_comments(comments, issuable_type)
+ def create_comments(comments)
ActiveRecord::Base.no_touching do
comments.each do |raw|
begin
- comment = CommentFormatter.new(project, raw)
- issuable_class = issuable_type == :issue ? Issue : MergeRequest
- iid = raw.send("#{issuable_type}_url").split('/').last # GH doesn't return parent ID directly
+ comment = CommentFormatter.new(project, raw)
+ # GH does not return info about comment's parent, so we guess it by checking its URL!
+ *_, parent, iid = URI(raw.html_url).path.split('/')
+ issuable_class = parent == 'issues' ? Issue : MergeRequest
issuable = issuable_class.find_by_iid(iid)
next unless issuable
@@ -169,6 +189,24 @@ module Gitlab
end
end
+ def discard_inserted_comments(comments, last_note)
+ last_note_attrs = nil
+
+ cut_off_index = comments.find_index do |raw|
+ comment = CommentFormatter.new(project, raw)
+ comment_attrs = comment.attributes
+ last_note_attrs ||= last_note.slice(*comment_attrs.keys)
+
+ comment_attrs.with_indifferent_access == last_note_attrs
+ end
+
+ # No matching resource in the collection, which means we got halted right on the end of the last page, so all good
+ return unless cut_off_index
+
+ # Otherwise, remove the resources we've already inserted
+ comments.shift(cut_off_index + 1)
+ end
+
def import_wiki
unless project.wiki.repository_exists?
wiki = WikiFormatter.new(project)
@@ -184,7 +222,7 @@ module Gitlab
end
def import_releases
- client.releases(repo, per_page: 100) do |releases|
+ fetch_resources(:releases, repo, per_page: 100) do |releases|
releases.each do |raw|
begin
gh_release = ReleaseFormatter.new(project, raw)
@@ -195,6 +233,47 @@ module Gitlab
end
end
end
+
+ def fetch_resources(resource_type, *opts)
+ return if imported?(resource_type)
+
+ opts.last.merge!(page: current_page(resource_type))
+
+ client.public_send(resource_type, *opts) do |resources|
+ yield resources
+ increment_page(resource_type)
+ end
+
+ imported!(resource_type)
+ end
+
+ def imported?(resource_type)
+ Rails.cache.read("#{cache_key_prefix}:#{resource_type}:imported")
+ end
+
+ def imported!(resource_type)
+ Rails.cache.write("#{cache_key_prefix}:#{resource_type}:imported", true, ex: 1.day)
+ end
+
+ def increment_page(resource_type)
+ key = "#{cache_key_prefix}:#{resource_type}:current-page"
+
+ # Rails.cache.increment calls INCRBY directly on the value stored under the key, which is
+ # a serialized ActiveSupport::Cache::Entry, so it will return an error by Redis, hence this ugly work-around
+ page = Rails.cache.read(key)
+ page += 1
+ Rails.cache.write(key, page)
+
+ page
+ end
+
+ def current_page(resource_type)
+ Rails.cache.fetch("#{cache_key_prefix}:#{resource_type}:current-page", ex: 1.day) { 1 }
+ end
+
+ def cache_key_prefix
+ @cache_key_prefix ||= "github-import:#{project.id}"
+ end
end
end
end
diff --git a/lib/gitlab/github_import/issue_formatter.rb b/lib/gitlab/github_import/issue_formatter.rb
index 77621de9f4c..8c32ac59fc5 100644
--- a/lib/gitlab/github_import/issue_formatter.rb
+++ b/lib/gitlab/github_import/issue_formatter.rb
@@ -20,8 +20,12 @@ module Gitlab
raw_data.comments > 0
end
- def klass
- Issue
+ def project_association
+ :issues
+ end
+
+ def find_condition
+ { iid: number }
end
def number
diff --git a/lib/gitlab/github_import/label_formatter.rb b/lib/gitlab/github_import/label_formatter.rb
index 942dfb3312b..211ccdc51bb 100644
--- a/lib/gitlab/github_import/label_formatter.rb
+++ b/lib/gitlab/github_import/label_formatter.rb
@@ -9,14 +9,14 @@ module Gitlab
}
end
- def klass
- Label
+ def project_association
+ :labels
end
def create!
params = attributes.except(:project)
- service = ::Labels::FindOrCreateService.new(project.owner, project, params)
- label = service.execute
+ service = ::Labels::FindOrCreateService.new(nil, project, params)
+ label = service.execute(skip_authorization: true)
raise ActiveRecord::RecordInvalid.new(label) unless label.persisted?
diff --git a/lib/gitlab/github_import/milestone_formatter.rb b/lib/gitlab/github_import/milestone_formatter.rb
index b2fa524cf5b..401dd962521 100644
--- a/lib/gitlab/github_import/milestone_formatter.rb
+++ b/lib/gitlab/github_import/milestone_formatter.rb
@@ -14,8 +14,12 @@ module Gitlab
}
end
- def klass
- Milestone
+ def project_association
+ :milestones
+ end
+
+ def find_condition
+ { iid: raw_data.number }
end
private
diff --git a/lib/gitlab/github_import/pull_request_formatter.rb b/lib/gitlab/github_import/pull_request_formatter.rb
index 1408683100f..b9a227fb11a 100644
--- a/lib/gitlab/github_import/pull_request_formatter.rb
+++ b/lib/gitlab/github_import/pull_request_formatter.rb
@@ -24,8 +24,12 @@ module Gitlab
}
end
- def klass
- MergeRequest
+ def project_association
+ :merge_requests
+ end
+
+ def find_condition
+ { iid: number }
end
def number
diff --git a/lib/gitlab/github_import/release_formatter.rb b/lib/gitlab/github_import/release_formatter.rb
index 73d643b00ad..1ad702a6058 100644
--- a/lib/gitlab/github_import/release_formatter.rb
+++ b/lib/gitlab/github_import/release_formatter.rb
@@ -11,8 +11,12 @@ module Gitlab
}
end
- def klass
- Release
+ def project_association
+ :releases
+ end
+
+ def find_condition
+ { tag: raw_data.tag_name }
end
def valid?
diff --git a/lib/gitlab/google_code_import/importer.rb b/lib/gitlab/google_code_import/importer.rb
index 6a68e786b4f..1f4edc36928 100644
--- a/lib/gitlab/google_code_import/importer.rb
+++ b/lib/gitlab/google_code_import/importer.rb
@@ -101,7 +101,7 @@ module Gitlab
state: raw_issue['state'] == 'closed' ? 'closed' : 'opened'
)
- issue_labels = ::LabelsFinder.new(project.owner, project_id: project.id, title: labels).execute
+ issue_labels = ::LabelsFinder.new(nil, project_id: project.id, title: labels).execute(skip_authorization: true)
issue.update_attribute(:label_ids, issue_labels.pluck(:id))
import_issue_comments(issue, comments)
@@ -235,7 +235,7 @@ module Gitlab
def create_label(name)
params = { name: name, color: nice_label_color(name) }
- ::Labels::FindOrCreateService.new(project.owner, project, params).execute
+ ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
end
def format_content(raw_content)
diff --git a/lib/gitlab/issues_labels.rb b/lib/gitlab/issues_labels.rb
index dbc759367eb..b8ca7f2f55f 100644
--- a/lib/gitlab/issues_labels.rb
+++ b/lib/gitlab/issues_labels.rb
@@ -19,7 +19,7 @@ module Gitlab
]
labels.each do |params|
- ::Labels::FindOrCreateService.new(project.owner, project, params).execute
+ ::Labels::FindOrCreateService.new(nil, project, params).execute(skip_authorization: true)
end
end
end
diff --git a/lib/gitlab/mail_room.rb b/lib/gitlab/mail_room.rb
index 12999a90a29..a5220d92312 100644
--- a/lib/gitlab/mail_room.rb
+++ b/lib/gitlab/mail_room.rb
@@ -33,7 +33,12 @@ module Gitlab
config[:mailbox] = 'inbox' if config[:mailbox].nil?
if config[:enabled] && config[:address]
- config[:redis_url] = Gitlab::Redis.new(rails_env).url
+ gitlab_redis = Gitlab::Redis.new(rails_env)
+ config[:redis_url] = gitlab_redis.url
+
+ if gitlab_redis.sentinels?
+ config[:sentinels] = gitlab_redis.sentinels
+ end
end
config
diff --git a/lib/gitlab/optimistic_locking.rb b/lib/gitlab/optimistic_locking.rb
new file mode 100644
index 00000000000..879d46446b3
--- /dev/null
+++ b/lib/gitlab/optimistic_locking.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module OptimisticLocking
+ extend self
+
+ def retry_lock(subject, retries = 100, &block)
+ loop do
+ begin
+ ActiveRecord::Base.transaction do
+ return block.call(subject)
+ end
+ rescue ActiveRecord::StaleObjectError
+ retries -= 1
+ raise unless retries >= 0
+ subject.reload
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index c649da8c426..9226da2d6b1 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -63,6 +63,14 @@ module Gitlab
raw_config_hash[:url]
end
+ def sentinels
+ raw_config_hash[:sentinels]
+ end
+
+ def sentinels?
+ sentinels && !sentinels.empty?
+ end
+
private
def redis_store_options
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index e59ead5d76c..4c395b4266e 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -13,5 +13,13 @@ module Gitlab
def force_utf8(str)
str.force_encoding(Encoding::UTF_8)
end
+
+ def to_boolean(value)
+ return value if [true, false].include?(value)
+ return true if value =~ /^(true|t|yes|y|1|on)$/i
+ return false if value =~ /^(false|f|no|n|0|off)$/i
+
+ nil
+ end
end
end
diff --git a/lib/tasks/eslint.rake b/lib/tasks/eslint.rake
new file mode 100644
index 00000000000..d43cbad1909
--- /dev/null
+++ b/lib/tasks/eslint.rake
@@ -0,0 +1,7 @@
+unless Rails.env.production?
+ desc "GitLab | Run ESLint"
+ task :eslint do
+ system("npm", "run", "eslint")
+ end
+end
+
diff --git a/lib/tasks/lint.rake b/lib/tasks/lint.rake
new file mode 100644
index 00000000000..32b668df3bf
--- /dev/null
+++ b/lib/tasks/lint.rake
@@ -0,0 +1,9 @@
+unless Rails.env.production?
+ namespace :lint do
+ desc "GitLab | lint | Lint JavaScript files using ESLint"
+ task :javascript do
+ Rake::Task['eslint'].invoke
+ end
+ end
+end
+
diff --git a/lib/tasks/teaspoon.rake b/lib/tasks/teaspoon.rake
new file mode 100644
index 00000000000..08caedd7ff3
--- /dev/null
+++ b/lib/tasks/teaspoon.rake
@@ -0,0 +1,25 @@
+unless Rails.env.production?
+ Rake::Task['teaspoon'].clear if Rake::Task.task_defined?('teaspoon')
+
+ namespace :teaspoon do
+ desc 'GitLab | Teaspoon | Generate fixtures for JavaScript tests'
+ RSpec::Core::RakeTask.new(:fixtures) do |t|
+ ENV['NO_KNAPSACK'] = 'true'
+ t.pattern = 'spec/javascripts/fixtures/*.rb'
+ t.rspec_opts = '--format documentation'
+ end
+
+ desc 'GitLab | Teaspoon | Run JavaScript tests'
+ task :tests do
+ require "teaspoon/console"
+ options = {}
+ abort('rake teaspoon:tests failed') if Teaspoon::Console.new(options).failures?
+ end
+ end
+
+ desc 'GitLab | Teaspoon | Shortcut for teaspoon:fixtures and teaspoon:tests'
+ task :teaspoon do
+ Rake::Task['teaspoon:fixtures'].invoke
+ Rake::Task['teaspoon:tests'].invoke
+ end
+end
diff --git a/package.json b/package.json
new file mode 100644
index 00000000000..d440307bd10
--- /dev/null
+++ b/package.json
@@ -0,0 +1,14 @@
+{
+ "private": true,
+ "scripts": {
+ "eslint": "eslint --ext .js,.js.es6 .",
+ "eslint-fix": "eslint --fix --ext .js,.js.es6 ."
+ },
+ "devDependencies": {
+ "eslint": "^3.1.1",
+ "eslint-config-airbnb": "^12.0.0",
+ "eslint-plugin-import": "^2.0.1",
+ "eslint-plugin-jsx-a11y": "^2.2.3",
+ "eslint-plugin-react": "^6.4.1"
+ }
+}
diff --git a/spec/config/mail_room_spec.rb b/spec/config/mail_room_spec.rb
index c5d3cd70acc..22bf3055538 100644
--- a/spec/config/mail_room_spec.rb
+++ b/spec/config/mail_room_spec.rb
@@ -3,6 +3,8 @@ require 'spec_helper'
describe 'mail_room.yml' do
let(:config_path) { 'config/mail_room.yml' }
let(:configuration) { YAML.load(ERB.new(File.read(config_path)).result) }
+ before(:each) { clear_raw_config }
+ after(:each) { clear_raw_config }
context 'when incoming email is disabled' do
before do
@@ -20,6 +22,9 @@ describe 'mail_room.yml' do
end
context 'when incoming email is enabled' do
+ let(:redis_config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+ let(:gitlab_redis) { Gitlab::Redis.new(Rails.env) }
+
before do
ENV['MAIL_ROOM_GITLAB_CONFIG_FILE'] = Rails.root.join('spec/fixtures/mail_room_enabled.yml').to_s
Gitlab::MailRoom.reset_config!
@@ -30,8 +35,9 @@ describe 'mail_room.yml' do
end
it 'contains the intended configuration' do
- expect(configuration[:mailboxes].length).to eq(1)
+ stub_const('Gitlab::Redis::CONFIG_FILE', redis_config)
+ expect(configuration[:mailboxes].length).to eq(1)
mailbox = configuration[:mailboxes].first
expect(mailbox[:host]).to eq('imap.gmail.com')
@@ -42,10 +48,26 @@ describe 'mail_room.yml' do
expect(mailbox[:password]).to eq('[REDACTED]')
expect(mailbox[:name]).to eq('inbox')
- redis_url = Gitlab::Redis.url
+ redis_url = gitlab_redis.url
+ sentinels = gitlab_redis.sentinels
+ expect(mailbox[:delivery_options][:redis_url]).to be_present
expect(mailbox[:delivery_options][:redis_url]).to eq(redis_url)
+
+ expect(mailbox[:delivery_options][:sentinels]).to be_present
+ expect(mailbox[:delivery_options][:sentinels]).to eq(sentinels)
+
+ expect(mailbox[:arbitration_options][:redis_url]).to be_present
expect(mailbox[:arbitration_options][:redis_url]).to eq(redis_url)
+
+ expect(mailbox[:arbitration_options][:sentinels]).to be_present
+ expect(mailbox[:arbitration_options][:sentinels]).to eq(sentinels)
end
end
+
+ def clear_raw_config
+ Gitlab::Redis.remove_instance_variable(:@_raw_config)
+ rescue NameError
+ # raised if @_raw_config was not set; ignore
+ end
end
diff --git a/spec/controllers/admin/users_controller_spec.rb b/spec/controllers/admin/users_controller_spec.rb
index 33fe3c73822..2ab2ca1b667 100644
--- a/spec/controllers/admin/users_controller_spec.rb
+++ b/spec/controllers/admin/users_controller_spec.rb
@@ -114,6 +114,17 @@ describe Admin::UsersController do
end
end
+ describe 'POST create' do
+ it 'creates the user' do
+ expect{ post :create, user: attributes_for(:user) }.to change{ User.count }.by(1)
+ end
+
+ it 'shows only one error message for an invalid email' do
+ post :create, user: attributes_for(:user, email: 'bogus')
+ expect(assigns[:user].errors).to contain_exactly("Email is invalid")
+ end
+ end
+
describe 'POST update' do
context 'when the password has changed' do
def update_password(user, password, password_confirmation = nil)
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index da59642f24d..cbe0417a4a7 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -21,9 +21,11 @@ describe Projects::Boards::IssuesController do
context 'with valid list id' do
it 'returns issues that have the list label applied' do
johndoe = create(:user, avatar: fixture_file_upload(File.join(Rails.root, 'spec/fixtures/dk.png')))
+ issue = create(:labeled_issue, project: project, labels: [planning])
create(:labeled_issue, project: project, labels: [planning])
- create(:labeled_issue, project: project, labels: [development])
+ create(:labeled_issue, project: project, labels: [development], due_date: Date.tomorrow)
create(:labeled_issue, project: project, labels: [development], assignee: johndoe)
+ issue.subscribe(johndoe)
list_issues user: user, board: board, list: list2
diff --git a/spec/controllers/projects/labels_controller_spec.rb b/spec/controllers/projects/labels_controller_spec.rb
index 41df63d445a..8faecec0063 100644
--- a/spec/controllers/projects/labels_controller_spec.rb
+++ b/spec/controllers/projects/labels_controller_spec.rb
@@ -2,7 +2,7 @@ require 'spec_helper'
describe Projects::LabelsController do
let(:group) { create(:group) }
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:empty_project, namespace: group) }
let(:user) { create(:user) }
before do
@@ -73,16 +73,27 @@ describe Projects::LabelsController do
describe 'POST #generate' do
let(:admin) { create(:admin) }
- let(:project) { create(:empty_project) }
before do
sign_in(admin)
end
- it 'creates labels' do
- post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param
+ context 'personal project' do
+ let(:personal_project) { create(:empty_project) }
- expect(response).to have_http_status(302)
+ it 'creates labels' do
+ post :generate, namespace_id: personal_project.namespace.to_param, project_id: personal_project.to_param
+
+ expect(response).to have_http_status(302)
+ end
+ end
+
+ context 'project belonging to a group' do
+ it 'creates labels' do
+ post :generate, namespace_id: project.namespace.to_param, project_id: project.to_param
+
+ expect(response).to have_http_status(302)
+ end
end
end
end
diff --git a/spec/controllers/projects/milestones_controller_spec.rb b/spec/controllers/projects/milestones_controller_spec.rb
index 4e3ef5dc6fa..7c5f33c63b8 100644
--- a/spec/controllers/projects/milestones_controller_spec.rb
+++ b/spec/controllers/projects/milestones_controller_spec.rb
@@ -20,7 +20,7 @@ describe Projects::MilestonesController do
delete :destroy, namespace_id: project.namespace.id, project_id: project.id, id: milestone.iid, format: :js
expect(response).to be_success
- expect(Event.first.action).to eq(Event::DESTROYED)
+ expect(Event.recent.first.action).to eq(Event::DESTROYED)
expect { Milestone.find(milestone.id) }.to raise_exception(ActiveRecord::RecordNotFound)
issue.reload
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index b4f066d8600..2a7523c6512 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -14,49 +14,49 @@ describe Projects::ProjectMembersController do
end
describe 'POST create' do
- context 'when users are added' do
- let(:project_user) { create(:user) }
+ let(:project_user) { create(:user) }
- before { sign_in(user) }
+ before { sign_in(user) }
- context 'when user does not have enough rights' do
- before { project.team << [user, :developer] }
+ context 'when user does not have enough rights' do
+ before { project.team << [user, :developer] }
- it 'returns 404' do
- post :create, namespace_id: project.namespace,
- project_id: project,
- user_ids: project_user.id,
- access_level: Gitlab::Access::GUEST
+ it 'returns 404' do
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: project_user.id,
+ access_level: Gitlab::Access::GUEST
- expect(response).to have_http_status(404)
- expect(project.users).not_to include project_user
- end
+ expect(response).to have_http_status(404)
+ expect(project.users).not_to include project_user
end
+ end
- context 'when user has enough rights' do
- before { project.team << [user, :master] }
+ context 'when user has enough rights' do
+ before { project.team << [user, :master] }
- it 'adds user to members' do
- post :create, namespace_id: project.namespace,
- project_id: project,
- user_ids: project_user.id,
- access_level: Gitlab::Access::GUEST
-
- expect(response).to set_flash.to 'Users were successfully added.'
- expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
- expect(project.users).to include project_user
- end
+ it 'adds user to members' do
+ expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(true)
- it 'adds no user to members' do
- post :create, namespace_id: project.namespace,
- project_id: project,
- user_ids: '',
- access_level: Gitlab::Access::GUEST
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: project_user.id,
+ access_level: Gitlab::Access::GUEST
- expect(response).to set_flash.to 'No users or groups specified.'
- expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
- expect(project.users).not_to include project_user
- end
+ expect(response).to set_flash.to 'Users were successfully added.'
+ expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
+ end
+
+ it 'adds no user to members' do
+ expect_any_instance_of(Members::CreateService).to receive(:execute).and_return(false)
+
+ post :create, namespace_id: project.namespace,
+ project_id: project,
+ user_ids: '',
+ access_level: Gitlab::Access::GUEST
+
+ expect(response).to set_flash.to 'No users or groups specified.'
+ expect(response).to redirect_to(namespace_project_project_members_path(project.namespace, project))
end
end
end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 41d263a46a4..2d762fdaa04 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -116,116 +116,126 @@ describe SnippetsController do
end
end
- describe 'GET #raw' do
- let(:user) { create(:user) }
+ %w(raw download).each do |action|
+ describe "GET #{action}" do
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- context 'when the personal snippet is private' do
- let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ it 'responds with status 404' do
+ get action, id: other_personal_snippet.to_param
- context 'when signed in user is not the author' do
- let(:other_author) { create(:author) }
- let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+ expect(response).to have_http_status(404)
+ end
+ end
- it 'responds with status 404' do
- get :raw, id: other_personal_snippet.to_param
+ context 'when signed in user is the author' do
+ before { get action, id: personal_snippet.to_param }
- expect(response).to have_http_status(404)
- end
- end
+ it 'responds with status 200' do
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
- context 'when signed in user is the author' do
- it 'renders the raw snippet' do
- get :raw, id: personal_snippet.to_param
+ it 'has expected headers' do
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ if action == :download
+ expect(response.header['Content-Disposition']).to match(/attachment/)
+ elsif action == :raw
+ expect(response.header['Content-Disposition']).to match(/inline/)
+ end
+ end
end
end
- end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get :raw, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get action, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to redirect_to(new_user_session_path)
+ end
end
end
- end
- context 'when the personal snippet is internal' do
- let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'renders the raw snippet' do
- get :raw, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
end
- end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get :raw, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get action, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
+ expect(response).to redirect_to(new_user_session_path)
+ end
end
end
- end
- context 'when the personal snippet is public' do
- let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'renders the raw snippet' do
- get :raw, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
end
- end
- context 'when not signed in' do
- it 'renders the raw snippet' do
- get :raw, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'responds with status 200' do
+ get action, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
end
end
- end
- context 'when the personal snippet does not exist' do
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 404' do
- get :raw, id: 'doesntexist'
+ it 'responds with status 404' do
+ get action, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
end
- end
- context 'when not signed in' do
- it 'responds with status 404' do
- get :raw, id: 'doesntexist'
+ context 'when not signed in' do
+ it 'responds with status 404' do
+ get action, id: 'doesntexist'
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(404)
+ end
end
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 4065e2defbc..dd4a86b1e31 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -118,10 +118,9 @@ FactoryGirl.define do
project.create_jira_service(
active: true,
properties: {
- 'title' => 'JIRA tracker',
- 'project_url' => 'http://jira.example/issues/?jql=project=A',
- 'issues_url' => 'http://jira.example/browse/:id',
- 'new_issue_url' => 'http://jira.example/secure/CreateIssue.jspa'
+ title: 'JIRA tracker',
+ url: 'http://jira.example.net',
+ project_key: 'JIRA'
}
)
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index c533ce1d87f..a92075fec8f 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -53,7 +53,7 @@ describe 'Issue Boards', feature: true, js: true do
context 'with lists' do
let(:milestone) { create(:milestone, project: project) }
- let(:planning) { create(:label, project: project, name: 'Planning') }
+ let(:planning) { create(:label, project: project, name: 'Planning', description: 'Test') }
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
let(:bug) { create(:label, project: project, name: 'Bug') }
@@ -91,6 +91,12 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.board', count: 4)
end
+ it 'shows description tooltip on list title' do
+ page.within('.board:nth-child(2)') do
+ expect(find('.board-title span.has-tooltip')[:title]).to eq('Test')
+ end
+ end
+
it 'shows issues in lists' do
wait_for_board_cards(2, 2)
wait_for_board_cards(3, 2)
@@ -347,6 +353,19 @@ describe 'Issue Boards', feature: true, js: true do
expect(page).to have_selector('.board', count: 5)
end
+ it 'keeps dropdown open after adding new list' do
+ click_button 'Create new list'
+ wait_for_ajax
+
+ page.within('.dropdown-menu-issues-board-new') do
+ click_link done.title
+ end
+
+ wait_for_vue_resource
+
+ expect(find('.issue-boards-search')).to have_selector('.open')
+ end
+
it 'moves issues from backlog into new list' do
wait_for_board_cards(1, 6)
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
index 67d6da5f39a..760a8967123 100644
--- a/spec/features/boards/new_issue_spec.rb
+++ b/spec/features/boards/new_issue_spec.rb
@@ -66,6 +66,21 @@ describe 'Issue Boards new issue', feature: true, js: true do
expect(page).to have_content('1')
end
end
+
+ it 'shows sidebar when creating new issue' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+ end
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('bug')
+ click_button 'Submit issue'
+ end
+
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+ end
end
context 'unauthorized user' do
diff --git a/spec/features/boards/sidebar_spec.rb b/spec/features/boards/sidebar_spec.rb
new file mode 100644
index 00000000000..f160052a844
--- /dev/null
+++ b/spec/features/boards/sidebar_spec.rb
@@ -0,0 +1,312 @@
+require 'rails_helper'
+
+describe 'Issue Boards', feature: true, js: true do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:empty_project, :public) }
+ let(:board) { create(:board, project: project) }
+ let(:user) { create(:user) }
+ let!(:label) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:issue2) { create(:labeled_issue, project: project, assignee: user, milestone: milestone, labels: [label]) }
+ let!(:issue) { create(:issue, project: project) }
+
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project, board)
+ wait_for_vue_resource
+ end
+
+ it 'shows sidebar when clicking issue' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'closes sidebar when clicking issue' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).not_to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'closes sidebar when clicking close button' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ expect(page).to have_selector('.issue-boards-sidebar')
+
+ find('.gutter-toggle').click
+
+ expect(page).not_to have_selector('.issue-boards-sidebar')
+ end
+
+ it 'shows issue details when sidebar is open' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.issue-boards-sidebar') do
+ expect(page).to have_content(issue.title)
+ expect(page).to have_content(issue.to_reference)
+ end
+ end
+
+ context 'assignee' do
+ it 'updates the issues assignee' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ page.within('.dropdown-menu-user') do
+ click_link user.name
+
+ wait_for_vue_resource
+ end
+
+ expect(page).to have_content(user.name)
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.avatar')
+ end
+ end
+ end
+
+ it 'removes the assignee' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.assignee') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ page.within('.dropdown-menu-user') do
+ click_link 'Unassigned'
+
+ wait_for_vue_resource
+ end
+
+ expect(page).to have_content('No assignee')
+ end
+
+ page.within(first('.board')) do
+ page.within(find('.card:nth-child(2)')) do
+ expect(page).not_to have_selector('.avatar')
+ end
+ end
+ end
+
+ it 'assignees to current user' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.assignee') do
+ click_link 'assign yourself'
+
+ wait_for_vue_resource
+
+ expect(page).to have_content(user.name)
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.avatar')
+ end
+ end
+ end
+ end
+
+ context 'milestone' do
+ it 'adds a milestone' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.milestone') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link milestone.title
+
+ wait_for_vue_resource
+
+ page.within('.value') do
+ expect(page).to have_content(milestone.title)
+ end
+ end
+ end
+
+ it 'removes a milestone' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.milestone') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link "No Milestone"
+
+ wait_for_vue_resource
+
+ page.within('.value') do
+ expect(page).not_to have_content(milestone.title)
+ end
+ end
+ end
+ end
+
+ context 'due date' do
+ it 'updates due date' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.due_date') do
+ click_link 'Edit'
+
+ click_link Date.today.day
+
+ wait_for_vue_resource
+
+ expect(page).to have_content(Date.today.to_s(:medium))
+ end
+ end
+ end
+
+ context 'labels' do
+ it 'adds a single label' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).to have_content(label.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.label', count: 1)
+ expect(page).to have_content(label.title)
+ end
+ end
+ end
+
+ it 'adds a multiple labels' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+ click_link label2.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(label.title)
+ expect(page).to have_content(label2.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(first('.card')) do
+ expect(page).to have_selector('.label', count: 2)
+ expect(page).to have_content(label.title)
+ expect(page).to have_content(label2.title)
+ end
+ end
+ end
+
+ it 'removes a label' do
+ page.within(first('.board')) do
+ find('.card:nth-child(2)').click
+ end
+
+ page.within('.labels') do
+ click_link 'Edit'
+
+ wait_for_ajax
+
+ click_link label.title
+
+ wait_for_vue_resource
+
+ find('.dropdown-menu-close-icon').click
+
+ page.within('.value') do
+ expect(page).to have_selector('.label', count: 0)
+ expect(page).not_to have_content(label.title)
+ end
+ end
+
+ page.within(first('.board')) do
+ page.within(find('.card:nth-child(2)')) do
+ expect(page).not_to have_selector('.label', count: 1)
+ expect(page).not_to have_content(label.title)
+ end
+ end
+ end
+ end
+
+ context 'subscription' do
+ it 'changes issue subscription' do
+ page.within(first('.board')) do
+ first('.card').click
+ end
+
+ page.within('.subscription') do
+ click_button 'Subscribe'
+
+ expect(page).to have_content("You're receiving notifications because you're subscribed to this thread.")
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/filter_by_milestone_spec.rb b/spec/features/issues/filter_by_milestone_spec.rb
index 485dc560061..88e1549a22b 100644
--- a/spec/features/issues/filter_by_milestone_spec.rb
+++ b/spec/features/issues/filter_by_milestone_spec.rb
@@ -58,6 +58,22 @@ feature 'Issue filtering by Milestone', feature: true do
expect(page).to have_css('.issue', count: 1)
end
+ context 'when milestone has single quotes in title' do
+ background do
+ milestone.update(name: "rock 'n' roll")
+ end
+
+ scenario 'filters by a specific Milestone', js: true do
+ create(:issue, project: project, milestone: milestone)
+ create(:issue, project: project)
+
+ visit_issues(project)
+ filter_by_milestone(milestone.title)
+
+ expect(page).to have_css('.issue', count: 1)
+ end
+ end
+
def visit_issues(project)
visit namespace_project_issues_path(project.namespace, project)
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index b963d1305b5..c68e1ea4af9 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -59,4 +59,12 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_css('a.btn.active', text: 'Side-by-side')
end
end
+
+ it 'does not allow non-existing branches' do
+ visit new_namespace_project_merge_request_path(project.namespace, project, merge_request: { target_branch: 'non-exist-target', source_branch: 'non-exist-source' })
+
+ expect(page).to have_content('The form contains the following errors')
+ expect(page).to have_content('Source branch "non-exist-source" does not exist')
+ expect(page).to have_content('Target branch "non-exist-target" does not exist')
+ end
end
diff --git a/spec/features/merge_requests/created_from_fork_spec.rb b/spec/features/merge_requests/created_from_fork_spec.rb
index cfc1244429f..142649297cc 100644
--- a/spec/features/merge_requests/created_from_fork_spec.rb
+++ b/spec/features/merge_requests/created_from_fork_spec.rb
@@ -40,8 +40,6 @@ feature 'Merge request created from fork' do
end
context 'pipeline present in source project' do
- include WaitForAjax
-
given(:pipeline) do
create(:ci_pipeline,
project: fork_project,
@@ -57,7 +55,6 @@ feature 'Merge request created from fork' do
scenario 'user visits a pipelines page', js: true do
visit_merge_request(merge_request)
page.within('.merge-request-tabs') { click_link 'Builds' }
- wait_for_ajax
page.within('table.ci-table') do
expect(page).to have_content 'rspec'
diff --git a/spec/features/merge_requests/diff_notes_resolve_spec.rb b/spec/features/merge_requests/diff_notes_resolve_spec.rb
index c6adf7e4c56..5e6d8467217 100644
--- a/spec/features/merge_requests/diff_notes_resolve_spec.rb
+++ b/spec/features/merge_requests/diff_notes_resolve_spec.rb
@@ -194,12 +194,12 @@ feature 'Diff notes resolve', feature: true, js: true do
context 'multiple notes' do
before do
create(:diff_note_on_merge_request, project: project, noteable: merge_request)
+ visit_merge_request
end
it 'does not mark discussion as resolved when resolving single note' do
- page.within '.diff-content .note' do
+ page.first '.diff-content .note' do
first('.line-resolve-btn').click
- sleep 1
expect(first('.line-resolve-btn')['data-original-title']).to eq("Resolved by #{user.name}")
end
@@ -212,7 +212,9 @@ feature 'Diff notes resolve', feature: true, js: true do
it 'resolves discussion' do
page.all('.note').each do |note|
- note.find('.line-resolve-btn').click
+ note.all('.line-resolve-btn').each do |button|
+ button.click
+ end
end
expect(page).to have_content('Resolved by')
@@ -292,7 +294,7 @@ feature 'Diff notes resolve', feature: true, js: true do
expect(holder).to have_selector('.discussion-next-btn')
end
end
-
+
it 'displays next discussion even if hidden' do
page.all('.note-discussion').each do |discussion|
page.within discussion do
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index d917d5950ec..f6e9230c8da 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -67,6 +67,23 @@ feature 'Merge Request filtering by Milestone', feature: true do
expect(page).to have_css('.merge-request', count: 1)
end
+ context 'when milestone has single quotes in title' do
+ background do
+ milestone.update(name: "rock 'n' roll")
+ end
+
+ scenario 'filters by a specific Milestone', js: true do
+ create(:merge_request, :with_diffs, source_project: project, milestone: milestone)
+ create(:merge_request, :simple, source_project: project)
+
+ visit_merge_requests(project)
+ filter_by_milestone(milestone.title)
+
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).to have_css('.merge-request', count: 1)
+ end
+ end
+
def visit_merge_requests(project)
visit namespace_project_merge_requests_path(project.namespace, project)
end
diff --git a/spec/features/milestones/milestones_spec.rb b/spec/features/milestones/milestones_spec.rb
new file mode 100644
index 00000000000..8b603f51545
--- /dev/null
+++ b/spec/features/milestones/milestones_spec.rb
@@ -0,0 +1,86 @@
+require 'rails_helper'
+
+describe 'Milestone draggable', feature: true, js: true do
+ let(:milestone) { create(:milestone, project: project, title: 8.14) }
+ let(:project) { create(:empty_project, :public) }
+ let(:user) { create(:user) }
+
+ context 'issues' do
+ let(:issue) { page.find_by_id('issues-list-unassigned').find('li') }
+ let(:issue_target) { page.find_by_id('issues-list-ongoing') }
+
+ it 'does not allow guest to drag issue' do
+ create_and_drag_issue
+
+ expect(issue_target).not_to have_selector('.issuable-row')
+ end
+
+ it 'does not allow authorized user to drag issue' do
+ login_as(user)
+ create_and_drag_issue
+
+ expect(issue_target).not_to have_selector('.issuable-row')
+ end
+
+ it 'allows author to drag issue' do
+ login_as(user)
+ create_and_drag_issue(author: user)
+
+ expect(issue_target).to have_selector('.issuable-row')
+ end
+
+ it 'allows admin to drag issue' do
+ login_as(:admin)
+ create_and_drag_issue
+
+ expect(issue_target).to have_selector('.issuable-row')
+ end
+ end
+
+ context 'merge requests' do
+ let(:merge_request) { page.find_by_id('merge_requests-list-unassigned').find('li') }
+ let(:merge_request_target) { page.find_by_id('merge_requests-list-ongoing') }
+
+ it 'does not allow guest to drag merge request' do
+ create_and_drag_merge_request
+
+ expect(merge_request_target).not_to have_selector('.issuable-row')
+ end
+
+ it 'does not allow authorized user to drag merge request' do
+ login_as(user)
+ create_and_drag_merge_request
+
+ expect(merge_request_target).not_to have_selector('.issuable-row')
+ end
+
+ it 'allows author to drag merge request' do
+ login_as(user)
+ create_and_drag_merge_request(author: user)
+
+ expect(merge_request_target).to have_selector('.issuable-row')
+ end
+
+ it 'allows admin to drag merge request' do
+ login_as(:admin)
+ create_and_drag_merge_request
+
+ expect(merge_request_target).to have_selector('.issuable-row')
+ end
+ end
+
+ def create_and_drag_issue(params = {})
+ create(:issue, params.merge(title: 'Foo', project: project, milestone: milestone))
+
+ visit namespace_project_milestone_path(project.namespace, project, milestone)
+ issue.drag_to(issue_target)
+ end
+
+ def create_and_drag_merge_request(params = {})
+ create(:merge_request, params.merge(title: 'Foo', source_project: project, target_project: project, milestone: milestone))
+
+ visit namespace_project_milestone_path(project.namespace, project, milestone)
+ page.find("a[href='#tab-merge-requests']").click
+ merge_request.drag_to(merge_request_target)
+ end
+end
diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb
deleted file mode 100644
index 63878c55421..00000000000
--- a/spec/features/projects/branches/delete_spec.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'spec_helper'
-
-feature 'Delete branch', feature: true, js: true do
- include WaitForAjax
-
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- before do
- project.team << [user, :master]
- login_as user
- visit namespace_project_branches_path(project.namespace, project)
- end
-
- it 'destroys tooltip' do
- first('.remove-row').hover
- expect(page).to have_selector('.tooltip')
-
- first('.remove-row').click
- wait_for_ajax
-
- expect(page).not_to have_selector('.tooltip')
- end
-end
diff --git a/spec/features/projects/builds_spec.rb b/spec/features/projects/builds_spec.rb
index d1685f95503..63a23a14f20 100644
--- a/spec/features/projects/builds_spec.rb
+++ b/spec/features/projects/builds_spec.rb
@@ -216,7 +216,9 @@ describe "Builds" do
@build.run!
visit namespace_project_build_path(@project.namespace, @project, @build)
click_link 'Cancel'
- click_link 'Retry'
+ page.within('.build-header') do
+ click_link 'Retry build'
+ end
end
it 'shows the right status and buttons' do
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 1d4484a9edd..e796ee570b7 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -41,6 +41,22 @@ describe 'Edit Project Settings', feature: true do
end
end
end
+
+ context "pipelines subtabs" do
+ it "shows builds when enabled" do
+ visit namespace_project_pipelines_path(project.namespace, project)
+
+ expect(page).to have_selector(".shortcuts-builds")
+ end
+
+ it "hides builds when disabled" do
+ allow(Ability).to receive(:allowed?).with(member, :read_builds, project).and_return(false)
+
+ visit namespace_project_pipelines_path(project.namespace, project)
+
+ expect(page).not_to have_selector(".shortcuts-builds")
+ end
+ end
end
describe 'project features visibility pages' do
@@ -148,5 +164,23 @@ describe 'Edit Project Settings', feature: true do
expect(page).to have_content "Customize your workflow!"
end
+
+ it "hides project activity tabs" do
+ select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+ select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+ select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+
+ click_button "Save changes"
+ wait_for_ajax
+
+ visit activity_namespace_project_path(project.namespace, project)
+
+ page.within(".event-filter") do
+ expect(page).to have_selector("a", count: 2)
+ expect(page).not_to have_content("Push events")
+ expect(page).not_to have_content("Merge events")
+ expect(page).not_to have_content("Comments")
+ end
+ end
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index d886909ce85..2f377312ea5 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -77,7 +77,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
- preview_template("#{prior_description}\n\n#{template_content}")
+ preview_template("#{template_content}")
save_changes
end
end
diff --git a/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
new file mode 100644
index 00000000000..b4f5f6b3fc5
--- /dev/null
+++ b/spec/features/projects/wiki/user_views_wiki_in_project_page_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Projects > Wiki > User views wiki in project page', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'when repository is disabled for project' do
+ before do
+ project.project_feature.update!(
+ repository_access_level: ProjectFeature::DISABLED,
+ merge_requests_access_level: ProjectFeature::DISABLED,
+ builds_access_level: ProjectFeature::DISABLED
+ )
+ end
+
+ context 'when wiki homepage contains a link' do
+ before do
+ WikiPages::CreateService.new(
+ project,
+ user,
+ title: 'home',
+ content: '[some link](other-page)'
+ ).execute
+ end
+
+ it 'displays the correct URL for the link' do
+ visit namespace_project_path(project.namespace, project)
+ expect(page).to have_link(
+ 'some link',
+ href: namespace_project_wiki_path(
+ project.namespace,
+ project,
+ 'other-page'
+ )
+ )
+ end
+ end
+ end
+end
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
index e74a51acede..fec28c55d30 100644
--- a/spec/features/todos/todos_sorting_spec.rb
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -8,60 +8,90 @@ describe "Dashboard > User sorts todos", feature: true do
let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
- let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
- let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
- let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
- let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
-
- let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
-
- before do
- create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
- create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
- create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
- create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
- create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
-
- merge_request_1.labels << label_1
- issue_3.labels << label_1
- issue_2.labels << label_3
- issue_1.labels << label_2
-
- project.team << [user, :developer]
- login_as(user)
- visit dashboard_todos_path
- end
+ before { project.team << [user, :developer] }
- it "sorts with oldest created todos first" do
- click_link "Last created"
+ context 'sort options' do
+ let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+ let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
+ let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
- results_list = page.find('.todos-list')
- expect(results_list.all('p')[0]).to have_content("merge_request_1")
- expect(results_list.all('p')[1]).to have_content("issue_1")
- expect(results_list.all('p')[2]).to have_content("issue_3")
- expect(results_list.all('p')[3]).to have_content("issue_2")
- expect(results_list.all('p')[4]).to have_content("issue_4")
- end
+ let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
+
+ before do
+ create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
+ create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
+ create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
+ create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
+ create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ merge_request_1.labels << label_1
+ issue_3.labels << label_1
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "sorts with oldest created todos first" do
+ click_link "Last created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("merge_request_1")
+ expect(results_list.all('p')[1]).to have_content("issue_1")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
- it "sorts with newest created todos first" do
- click_link "Oldest created"
+ it "sorts with newest created todos first" do
+ click_link "Oldest created"
- results_list = page.find('.todos-list')
- expect(results_list.all('p')[0]).to have_content("issue_4")
- expect(results_list.all('p')[1]).to have_content("issue_2")
- expect(results_list.all('p')[2]).to have_content("issue_3")
- expect(results_list.all('p')[3]).to have_content("issue_1")
- expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_4")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_1")
+ expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ end
+
+ it "sorts by priority" do
+ click_link "Priority"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_3")
+ expect(results_list.all('p')[1]).to have_content("merge_request_1")
+ expect(results_list.all('p')[2]).to have_content("issue_1")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
end
- it "sorts by priority" do
- click_link "Priority"
+ context 'issues and merge requests' do
+ let(:issue_1) { create(:issue, id: 10000, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, id: 10001, title: 'issue_2', project: project) }
+ let(:merge_request_1) { create(:merge_request, id: 10000, title: 'merge_request_1', source_project: project) }
+
+ before do
+ issue_1.labels << label_1
+ issue_2.labels << label_2
+
+ create(:todo, user: user, project: project, target: issue_1)
+ create(:todo, user: user, project: project, target: issue_2)
+ create(:todo, user: user, project: project, target: merge_request_1)
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "doesn't mix issues and merge requests priorities" do
+ click_link "Priority"
- results_list = page.find('.todos-list')
- expect(results_list.all('p')[0]).to have_content("issue_3")
- expect(results_list.all('p')[1]).to have_content("merge_request_1")
- expect(results_list.all('p')[2]).to have_content("issue_1")
- expect(results_list.all('p')[3]).to have_content("issue_2")
- expect(results_list.all('p')[4]).to have_content("issue_4")
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_1")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("merge_request_1")
+ end
end
end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index bf93c1d1251..3ae83ac082d 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -13,7 +13,7 @@ describe 'Dashboard Todos', feature: true do
visit dashboard_todos_path
end
it 'shows "All done" message' do
- expect(page).to have_content "You're all done!"
+ expect(page).to have_content "Todos let you see what you should do next."
end
end
@@ -44,7 +44,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- expect(page).to have_content("You're all done!")
+ expect(page).to have_content("Good job! Looks like you don't have any todos left.")
end
end
@@ -64,7 +64,7 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message' do
- expect(page).to have_content("You're all done!")
+ expect(page).to have_content("Good job! Looks like you don't have any todos left.")
end
end
end
@@ -132,7 +132,6 @@ describe 'Dashboard Todos', feature: true do
end
it 'shows "All done" message!' do
- within('.todos-pending-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content "You're all done!"
expect(page).not_to have_selector('.gl-pagination')
@@ -153,7 +152,7 @@ describe 'Dashboard Todos', feature: true do
within('.todos-pending-count') { expect(page).to have_content '0' }
expect(page).to have_content 'To do 0'
expect(page).to have_content 'Done 0'
- expect(page).to have_content "You're all done!"
+ expect(page).to have_content "Good job! Looks like you don't have any todos left."
end
end
end
diff --git a/spec/finders/branches_finder_spec.rb b/spec/finders/branches_finder_spec.rb
index 6fce11de30f..db60c01db0d 100644
--- a/spec/finders/branches_finder_spec.rb
+++ b/spec/finders/branches_finder_spec.rb
@@ -21,7 +21,7 @@ describe BranchesFinder do
result = branches_finder.execute
recently_updated_branch = repository.branches.max do |a, b|
- repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date
+ repository.commit(a.dereferenced_target).committed_date <=> repository.commit(b.dereferenced_target).committed_date
end
expect(result.first.name).to eq(recently_updated_branch.name)
diff --git a/spec/finders/tags_finder_spec.rb b/spec/finders/tags_finder_spec.rb
index 2ac810e478a..98b42e264dc 100644
--- a/spec/finders/tags_finder_spec.rb
+++ b/spec/finders/tags_finder_spec.rb
@@ -20,7 +20,7 @@ describe TagsFinder do
result = tags_finder.execute
recently_updated_tag = repository.tags.max do |a, b|
- repository.commit(a.target).committed_date <=> repository.commit(b.target).committed_date
+ repository.commit(a.dereferenced_target).committed_date <=> repository.commit(b.dereferenced_target).committed_date
end
expect(result.first.name).to eq(recently_updated_tag.name)
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 532ebb9640e..77f2bcee1f3 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -9,6 +9,7 @@
"iid": { "type": "integer" },
"title": { "type": "string" },
"confidential": { "type": "boolean" },
+ "due_date": { "type": ["date", "null"] },
"labels": {
"type": "array",
"items": {
@@ -42,7 +43,8 @@
"name": { "type": "string" },
"username": { "type": "string" },
"avatar_url": { "type": "uri" }
- }
+ },
+ "subscribed": { "type": ["boolean", "null"] }
},
"additionalProperties": false
}
diff --git a/spec/javascripts/abuse_reports_spec.js.es6 b/spec/javascripts/abuse_reports_spec.js.es6
index 6bcfdf191c2..a3171353bfb 100644
--- a/spec/javascripts/abuse_reports_spec.js.es6
+++ b/spec/javascripts/abuse_reports_spec.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require abuse_reports */
/*= require jquery */
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
index 743b15460c6..9d855ef1060 100644
--- a/spec/javascripts/activities_spec.js.es6
+++ b/spec/javascripts/activities_spec.js.es6
@@ -1,4 +1,5 @@
-/*= require jquery.cookie.js */
+/* eslint-disable */
+/*= require js.cookie.js */
/*= require jquery.endless-scroll.js */
/*= require pager */
/*= require activities */
diff --git a/spec/javascripts/application_spec.js b/spec/javascripts/application_spec.js
index 56b98856614..16e908f3a81 100644
--- a/spec/javascripts/application_spec.js
+++ b/spec/javascripts/application_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require lib/utils/common_utils */
diff --git a/spec/javascripts/awards_handler_spec.js b/spec/javascripts/awards_handler_spec.js
index 019ce3b0702..3d705e1cb2e 100644
--- a/spec/javascripts/awards_handler_spec.js
+++ b/spec/javascripts/awards_handler_spec.js
@@ -1,7 +1,8 @@
+/* eslint-disable */
/*= require awards_handler */
/*= require jquery */
-/*= require jquery.cookie */
+/*= require js.cookie */
/*= require ./fixtures/emoji_menu */
(function() {
@@ -44,7 +45,6 @@
spyOn(jQuery, 'get').and.callFake(function(req, cb) {
return cb(window.emojiMenu);
});
- spyOn(jQuery, 'cookie');
});
afterEach(function() {
// restore original url root value
@@ -190,28 +190,6 @@
return expect($thumbsUpEmoji.data("original-title")).toBe('sam');
});
});
- describe('::addEmojiToFrequentlyUsedList', function() {
- it('should set a cookie with the correct default path', function() {
- gon.relative_url_root = '';
- awardsHandler.addEmojiToFrequentlyUsedList('sunglasses');
- expect(jQuery.cookie)
- .toHaveBeenCalledWith('frequently_used_emojis', 'sunglasses', {
- path: '/',
- expires: 365
- })
- ;
- });
- it('should set a cookie with the correct custom root path', function() {
- gon.relative_url_root = '/gitlab/subdir';
- awardsHandler.addEmojiToFrequentlyUsedList('alien');
- expect(jQuery.cookie)
- .toHaveBeenCalledWith('frequently_used_emojis', 'alien', {
- path: '/gitlab/subdir',
- expires: 365
- })
- ;
- });
- });
describe('search', function() {
return it('should filter the emoji', function() {
$('.js-add-award').eq(0).click();
diff --git a/spec/javascripts/behaviors/autosize_spec.js b/spec/javascripts/behaviors/autosize_spec.js
index 78795f7654a..36254a7370e 100644
--- a/spec/javascripts/behaviors/autosize_spec.js
+++ b/spec/javascripts/behaviors/autosize_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require behaviors/autosize */
diff --git a/spec/javascripts/behaviors/quick_submit_spec.js b/spec/javascripts/behaviors/quick_submit_spec.js
index 13babb5bfdb..7370ccb4a08 100644
--- a/spec/javascripts/behaviors/quick_submit_spec.js
+++ b/spec/javascripts/behaviors/quick_submit_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require behaviors/quick_submit */
diff --git a/spec/javascripts/behaviors/requires_input_spec.js b/spec/javascripts/behaviors/requires_input_spec.js
index 724c3baf989..32469a4fd1f 100644
--- a/spec/javascripts/behaviors/requires_input_spec.js
+++ b/spec/javascripts/behaviors/requires_input_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require behaviors/requires_input */
diff --git a/spec/javascripts/boards/boards_store_spec.js.es6 b/spec/javascripts/boards/boards_store_spec.js.es6
index 15c305ce321..6208c2386b0 100644
--- a/spec/javascripts/boards/boards_store_spec.js.es6
+++ b/spec/javascripts/boards/boards_store_spec.js.es6
@@ -1,6 +1,7 @@
+/* eslint-disable */
//= require jquery
//= require jquery_ujs
-//= require jquery.cookie
+//= require js.cookie
//= require vue
//= require vue-resource
//= require lib/utils/url_utility
@@ -17,7 +18,10 @@
gl.boardService = new BoardService('/test/issue-boards/board', '1');
gl.issueBoards.BoardsStore.create();
- $.cookie('issue_board_welcome_hidden', 'false');
+ Cookies.set('issue_board_welcome_hidden', 'false', {
+ expires: 365 * 10,
+ path: ''
+ });
});
describe('Store', () => {
diff --git a/spec/javascripts/boards/issue_spec.js.es6 b/spec/javascripts/boards/issue_spec.js.es6
index 328c6f82ab5..90cb8926545 100644
--- a/spec/javascripts/boards/issue_spec.js.es6
+++ b/spec/javascripts/boards/issue_spec.js.es6
@@ -1,6 +1,7 @@
+/* eslint-disable */
//= require jquery
//= require jquery_ujs
-//= require jquery.cookie
+//= require js.cookie
//= require vue
//= require vue-resource
//= require lib/utils/url_utility
diff --git a/spec/javascripts/boards/list_spec.js.es6 b/spec/javascripts/boards/list_spec.js.es6
index ec78d82e919..1a0427fdd90 100644
--- a/spec/javascripts/boards/list_spec.js.es6
+++ b/spec/javascripts/boards/list_spec.js.es6
@@ -1,6 +1,7 @@
+/* eslint-disable */
//= require jquery
//= require jquery_ujs
-//= require jquery.cookie
+//= require js.cookie
//= require vue
//= require vue-resource
//= require lib/utils/url_utility
diff --git a/spec/javascripts/boards/mock_data.js.es6 b/spec/javascripts/boards/mock_data.js.es6
index 052455f2ca6..80d05e8a1a3 100644
--- a/spec/javascripts/boards/mock_data.js.es6
+++ b/spec/javascripts/boards/mock_data.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
const listObj = {
id: 1,
position: 0,
diff --git a/spec/javascripts/dashboard_spec.js.es6 b/spec/javascripts/dashboard_spec.js.es6
new file mode 100644
index 00000000000..93f73fa0e9a
--- /dev/null
+++ b/spec/javascripts/dashboard_spec.js.es6
@@ -0,0 +1,39 @@
+/* eslint-disable */
+/*= require sidebar */
+/*= require jquery */
+/*= require js.cookie */
+/*= require lib/utils/text_utility */
+
+((global) => {
+ describe('Dashboard', () => {
+ const fixtureTemplate = 'dashboard.html';
+
+ function todosCountText() {
+ return $('.js-todos-count').text();
+ }
+
+ function triggerToggle(newCount) {
+ $(document).trigger('todo:toggle', newCount);
+ }
+
+ fixture.preload(fixtureTemplate);
+ beforeEach(() => {
+ fixture.load(fixtureTemplate);
+ new global.Sidebar();
+ });
+
+ it('should update todos-count after receiving the todo:toggle event', () => {
+ triggerToggle(5);
+ expect(todosCountText()).toEqual('5');
+ });
+
+ it('should display todos-count with delimiter', () => {
+ triggerToggle(1000);
+ expect(todosCountText()).toEqual('1,000');
+
+ triggerToggle(1000000);
+ expect(todosCountText()).toEqual('1,000,000');
+ });
+ });
+
+})(window.gl);
diff --git a/spec/javascripts/datetime_utility_spec.js.es6 b/spec/javascripts/datetime_utility_spec.js.es6
index a2d1b0a7732..9fdbab3a9e9 100644
--- a/spec/javascripts/datetime_utility_spec.js.es6
+++ b/spec/javascripts/datetime_utility_spec.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require lib/utils/datetime_utility
(() => {
describe('Date time utils', () => {
diff --git a/spec/javascripts/diff_comments_store_spec.js.es6 b/spec/javascripts/diff_comments_store_spec.js.es6
index 22293d4de87..5d817802602 100644
--- a/spec/javascripts/diff_comments_store_spec.js.es6
+++ b/spec/javascripts/diff_comments_store_spec.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require vue
//= require diff_notes/models/discussion
//= require diff_notes/models/note
diff --git a/spec/javascripts/extensions/array_spec.js b/spec/javascripts/extensions/array_spec.js
index eced2f6575d..f28983d7764 100644
--- a/spec/javascripts/extensions/array_spec.js
+++ b/spec/javascripts/extensions/array_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require extensions/array */
diff --git a/spec/javascripts/extensions/jquery_spec.js b/spec/javascripts/extensions/jquery_spec.js
index b644344b95a..9c361bb0867 100644
--- a/spec/javascripts/extensions/jquery_spec.js
+++ b/spec/javascripts/extensions/jquery_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require extensions/jquery */
diff --git a/spec/javascripts/fixtures/.gitignore b/spec/javascripts/fixtures/.gitignore
new file mode 100644
index 00000000000..009b68d5d1c
--- /dev/null
+++ b/spec/javascripts/fixtures/.gitignore
@@ -0,0 +1 @@
+*.html.raw
diff --git a/spec/javascripts/fixtures/dashboard.html.haml b/spec/javascripts/fixtures/dashboard.html.haml
new file mode 100644
index 00000000000..32446acfd60
--- /dev/null
+++ b/spec/javascripts/fixtures/dashboard.html.haml
@@ -0,0 +1,45 @@
+%ul.nav.nav-sidebar
+ %li.home.active
+ %a.dashboard-shortcuts-projects
+ %span
+ Projects
+ %li
+ %a
+ %span
+ Todos
+ %span.count.js-todos-count
+ 1
+ %li
+ %a.dashboard-shortcuts-activity
+ %span
+ Activity
+ %li
+ %a
+ %span
+ Groups
+ %li
+ %a
+ %span
+ Milestones
+ %li
+ %a.dashboard-shortcuts-issues
+ %span
+ Issues
+ %span
+ 1
+ %li
+ %a.dashboard-shortcuts-merge_requests
+ %span
+ Merge Requests
+ %li
+ %a
+ %span
+ Snippets
+ %li
+ %a
+ %span
+ Help
+ %li
+ %a
+ %span
+ Profile Settings
diff --git a/spec/javascripts/fixtures/emoji_menu.js b/spec/javascripts/fixtures/emoji_menu.js
index 99e3f7247bd..41cf40c29cf 100644
--- a/spec/javascripts/fixtures/emoji_menu.js
+++ b/spec/javascripts/fixtures/emoji_menu.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
window.emojiMenu = "<div class='emoji-menu'>\n <input type=\"text\" name=\"emoji_search\" id=\"emoji_search\" value=\"\" class=\"emoji-search search-input form-control\" />\n <div class='emoji-menu-content'>\n <h5 class='emoji-menu-title'>\n Emoticons\n </h5>\n <ul class='clearfix emoji-menu-list'>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47D\" title=\"alien\" data-aliases=\"\" data-emoji=\"alien\" data-unicode-name=\"1F47D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47C\" title=\"angel\" data-aliases=\"\" data-emoji=\"angel\" data-unicode-name=\"1F47C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A2\" title=\"anger\" data-aliases=\"\" data-emoji=\"anger\" data-unicode-name=\"1F4A2\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F620\" title=\"angry\" data-aliases=\"\" data-emoji=\"angry\" data-unicode-name=\"1F620\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F627\" title=\"anguished\" data-aliases=\"\" data-emoji=\"anguished\" data-unicode-name=\"1F627\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F632\" title=\"astonished\" data-aliases=\"\" data-emoji=\"astonished\" data-unicode-name=\"1F632\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45F\" title=\"athletic_shoe\" data-aliases=\"\" data-emoji=\"athletic_shoe\" data-unicode-name=\"1F45F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F476\" title=\"baby\" data-aliases=\"\" data-emoji=\"baby\" data-unicode-name=\"1F476\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F459\" title=\"bikini\" data-aliases=\"\" data-emoji=\"bikini\" data-unicode-name=\"1F459\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F499\" title=\"blue_heart\" data-aliases=\"\" data-emoji=\"blue_heart\" data-unicode-name=\"1F499\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60A\" title=\"blush\" data-aliases=\"\" data-emoji=\"blush\" data-unicode-name=\"1F60A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A5\" title=\"boom\" data-aliases=\"\" data-emoji=\"boom\" data-unicode-name=\"1F4A5\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F462\" title=\"boot\" data-aliases=\"\" data-emoji=\"boot\" data-unicode-name=\"1F462\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F647\" title=\"bow\" data-aliases=\"\" data-emoji=\"bow\" data-unicode-name=\"1F647\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F466\" title=\"boy\" data-aliases=\"\" data-emoji=\"boy\" data-unicode-name=\"1F466\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F470\" title=\"bride_with_veil\" data-aliases=\"\" data-emoji=\"bride_with_veil\" data-unicode-name=\"1F470\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4BC\" title=\"briefcase\" data-aliases=\"\" data-emoji=\"briefcase\" data-unicode-name=\"1F4BC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F494\" title=\"broken_heart\" data-aliases=\"\" data-emoji=\"broken_heart\" data-unicode-name=\"1F494\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F464\" title=\"bust_in_silhouette\" data-aliases=\"\" data-emoji=\"bust_in_silhouette\" data-unicode-name=\"1F464\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F465\" title=\"busts_in_silhouette\" data-aliases=\"\" data-emoji=\"busts_in_silhouette\" data-unicode-name=\"1F465\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44F\" title=\"clap\" data-aliases=\"\" data-emoji=\"clap\" data-unicode-name=\"1F44F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F302\" title=\"closed_umbrella\" data-aliases=\"\" data-emoji=\"closed_umbrella\" data-unicode-name=\"1F302\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F630\" title=\"cold_sweat\" data-aliases=\"\" data-emoji=\"cold_sweat\" data-unicode-name=\"1F630\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F616\" title=\"confounded\" data-aliases=\"\" data-emoji=\"confounded\" data-unicode-name=\"1F616\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F615\" title=\"confused\" data-aliases=\"\" data-emoji=\"confused\" data-unicode-name=\"1F615\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F477\" title=\"construction_worker\" data-aliases=\"\" data-emoji=\"construction_worker\" data-unicode-name=\"1F477\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46E\" title=\"cop\" data-aliases=\"\" data-emoji=\"cop\" data-unicode-name=\"1F46E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46B\" title=\"couple\" data-aliases=\"\" data-emoji=\"couple\" data-unicode-name=\"1F46B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F491\" title=\"couple_with_heart\" data-aliases=\"\" data-emoji=\"couple_with_heart\" data-unicode-name=\"1F491\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48F\" title=\"couplekiss\" data-aliases=\"\" data-emoji=\"couplekiss\" data-unicode-name=\"1F48F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F451\" title=\"crown\" data-aliases=\"\" data-emoji=\"crown\" data-unicode-name=\"1F451\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F622\" title=\"cry\" data-aliases=\"\" data-emoji=\"cry\" data-unicode-name=\"1F622\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63F\" title=\"crying_cat_face\" data-aliases=\"\" data-emoji=\"crying_cat_face\" data-unicode-name=\"1F63F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F498\" title=\"cupid\" data-aliases=\"\" data-emoji=\"cupid\" data-unicode-name=\"1F498\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F483\" title=\"dancer\" data-aliases=\"\" data-emoji=\"dancer\" data-unicode-name=\"1F483\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46F\" title=\"dancers\" data-aliases=\"\" data-emoji=\"dancers\" data-unicode-name=\"1F46F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A8\" title=\"dash\" data-aliases=\"\" data-emoji=\"dash\" data-unicode-name=\"1F4A8\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61E\" title=\"disappointed\" data-aliases=\"\" data-emoji=\"disappointed\" data-unicode-name=\"1F61E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F625\" title=\"disappointed_relieved\" data-aliases=\"\" data-emoji=\"disappointed_relieved\" data-unicode-name=\"1F625\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AB\" title=\"dizzy\" data-aliases=\"\" data-emoji=\"dizzy\" data-unicode-name=\"1F4AB\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F635\" title=\"dizzy_face\" data-aliases=\"\" data-emoji=\"dizzy_face\" data-unicode-name=\"1F635\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F457\" title=\"dress\" data-aliases=\"\" data-emoji=\"dress\" data-unicode-name=\"1F457\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A7\" title=\"droplet\" data-aliases=\"\" data-emoji=\"droplet\" data-unicode-name=\"1F4A7\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F442\" title=\"ear\" data-aliases=\"\" data-emoji=\"ear\" data-unicode-name=\"1F442\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F611\" title=\"expressionless\" data-aliases=\"\" data-emoji=\"expressionless\" data-unicode-name=\"1F611\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F453\" title=\"eyeglasses\" data-aliases=\"\" data-emoji=\"eyeglasses\" data-unicode-name=\"1F453\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F440\" title=\"eyes\" data-aliases=\"\" data-emoji=\"eyes\" data-unicode-name=\"1F440\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46A\" title=\"family\" data-aliases=\"\" data-emoji=\"family\" data-unicode-name=\"1F46A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F628\" title=\"fearful\" data-aliases=\"\" data-emoji=\"fearful\" data-unicode-name=\"1F628\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F525\" title=\"fire\" data-aliases=\":flame:\" data-emoji=\"fire\" data-unicode-name=\"1F525\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270A\" title=\"fist\" data-aliases=\"\" data-emoji=\"fist\" data-unicode-name=\"270A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F633\" title=\"flushed\" data-aliases=\"\" data-emoji=\"flushed\" data-unicode-name=\"1F633\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F463\" title=\"footprints\" data-aliases=\"\" data-emoji=\"footprints\" data-unicode-name=\"1F463\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F626\" title=\"frowning\" data-aliases=\":anguished:\" data-emoji=\"frowning\" data-unicode-name=\"1F626\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48E\" title=\"gem\" data-aliases=\"\" data-emoji=\"gem\" data-unicode-name=\"1F48E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F467\" title=\"girl\" data-aliases=\"\" data-emoji=\"girl\" data-unicode-name=\"1F467\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49A\" title=\"green_heart\" data-aliases=\"\" data-emoji=\"green_heart\" data-unicode-name=\"1F49A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62C\" title=\"grimacing\" data-aliases=\"\" data-emoji=\"grimacing\" data-unicode-name=\"1F62C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F601\" title=\"grin\" data-aliases=\"\" data-emoji=\"grin\" data-unicode-name=\"1F601\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F600\" title=\"grinning\" data-aliases=\"\" data-emoji=\"grinning\" data-unicode-name=\"1F600\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F482\" title=\"guardsman\" data-aliases=\"\" data-emoji=\"guardsman\" data-unicode-name=\"1F482\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F487\" title=\"haircut\" data-aliases=\"\" data-emoji=\"haircut\" data-unicode-name=\"1F487\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45C\" title=\"handbag\" data-aliases=\"\" data-emoji=\"handbag\" data-unicode-name=\"1F45C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F649\" title=\"hear_no_evil\" data-aliases=\"\" data-emoji=\"hear_no_evil\" data-unicode-name=\"1F649\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2764\" title=\"heart\" data-aliases=\"\" data-emoji=\"heart\" data-unicode-name=\"2764\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60D\" title=\"heart_eyes\" data-aliases=\"\" data-emoji=\"heart_eyes\" data-unicode-name=\"1F60D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63B\" title=\"heart_eyes_cat\" data-aliases=\"\" data-emoji=\"heart_eyes_cat\" data-unicode-name=\"1F63B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F493\" title=\"heartbeat\" data-aliases=\"\" data-emoji=\"heartbeat\" data-unicode-name=\"1F493\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F497\" title=\"heartpulse\" data-aliases=\"\" data-emoji=\"heartpulse\" data-unicode-name=\"1F497\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F460\" title=\"high_heel\" data-aliases=\"\" data-emoji=\"high_heel\" data-unicode-name=\"1F460\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62F\" title=\"hushed\" data-aliases=\"\" data-emoji=\"hushed\" data-unicode-name=\"1F62F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47F\" title=\"imp\" data-aliases=\"\" data-emoji=\"imp\" data-unicode-name=\"1F47F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F481\" title=\"information_desk_person\" data-aliases=\"\" data-emoji=\"information_desk_person\" data-unicode-name=\"1F481\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F607\" title=\"innocent\" data-aliases=\"\" data-emoji=\"innocent\" data-unicode-name=\"1F607\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F47A\" title=\"japanese_goblin\" data-aliases=\"\" data-emoji=\"japanese_goblin\" data-unicode-name=\"1F47A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F479\" title=\"japanese_ogre\" data-aliases=\"\" data-emoji=\"japanese_ogre\" data-unicode-name=\"1F479\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F456\" title=\"jeans\" data-aliases=\"\" data-emoji=\"jeans\" data-unicode-name=\"1F456\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F602\" title=\"joy\" data-aliases=\"\" data-emoji=\"joy\" data-unicode-name=\"1F602\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F639\" title=\"joy_cat\" data-aliases=\"\" data-emoji=\"joy_cat\" data-unicode-name=\"1F639\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F458\" title=\"kimono\" data-aliases=\"\" data-emoji=\"kimono\" data-unicode-name=\"1F458\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48B\" title=\"kiss\" data-aliases=\"\" data-emoji=\"kiss\" data-unicode-name=\"1F48B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F617\" title=\"kissing\" data-aliases=\"\" data-emoji=\"kissing\" data-unicode-name=\"1F617\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63D\" title=\"kissing_cat\" data-aliases=\"\" data-emoji=\"kissing_cat\" data-unicode-name=\"1F63D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61A\" title=\"kissing_closed_eyes\" data-aliases=\"\" data-emoji=\"kissing_closed_eyes\" data-unicode-name=\"1F61A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F618\" title=\"kissing_heart\" data-aliases=\"\" data-emoji=\"kissing_heart\" data-unicode-name=\"1F618\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F619\" title=\"kissing_smiling_eyes\" data-aliases=\"\" data-emoji=\"kissing_smiling_eyes\" data-unicode-name=\"1F619\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F606\" title=\"laughing\" data-aliases=\":satisfied:\" data-emoji=\"laughing\" data-unicode-name=\"1F606\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F444\" title=\"lips\" data-aliases=\"\" data-emoji=\"lips\" data-unicode-name=\"1F444\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F484\" title=\"lipstick\" data-aliases=\"\" data-emoji=\"lipstick\" data-unicode-name=\"1F484\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48C\" title=\"love_letter\" data-aliases=\"\" data-emoji=\"love_letter\" data-unicode-name=\"1F48C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F468\" title=\"man\" data-aliases=\"\" data-emoji=\"man\" data-unicode-name=\"1F468\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F472\" title=\"man_with_gua_pi_mao\" data-aliases=\"\" data-emoji=\"man_with_gua_pi_mao\" data-unicode-name=\"1F472\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F473\" title=\"man_with_turban\" data-aliases=\"\" data-emoji=\"man_with_turban\" data-unicode-name=\"1F473\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45E\" title=\"mans_shoe\" data-aliases=\"\" data-emoji=\"mans_shoe\" data-unicode-name=\"1F45E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F637\" title=\"mask\" data-aliases=\"\" data-emoji=\"mask\" data-unicode-name=\"1F637\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F486\" title=\"massage\" data-aliases=\"\" data-emoji=\"massage\" data-unicode-name=\"1F486\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AA\" title=\"muscle\" data-aliases=\"\" data-emoji=\"muscle\" data-unicode-name=\"1F4AA\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F485\" title=\"nail_care\" data-aliases=\"\" data-emoji=\"nail_care\" data-unicode-name=\"1F485\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F454\" title=\"necktie\" data-aliases=\"\" data-emoji=\"necktie\" data-unicode-name=\"1F454\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F610\" title=\"neutral_face\" data-aliases=\"\" data-emoji=\"neutral_face\" data-unicode-name=\"1F610\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F645\" title=\"no_good\" data-aliases=\"\" data-emoji=\"no_good\" data-unicode-name=\"1F645\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F636\" title=\"no_mouth\" data-aliases=\"\" data-emoji=\"no_mouth\" data-unicode-name=\"1F636\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F443\" title=\"nose\" data-aliases=\"\" data-emoji=\"nose\" data-unicode-name=\"1F443\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44C\" title=\"ok_hand\" data-aliases=\"\" data-emoji=\"ok_hand\" data-unicode-name=\"1F44C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F646\" title=\"ok_woman\" data-aliases=\"\" data-emoji=\"ok_woman\" data-unicode-name=\"1F646\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F474\" title=\"older_man\" data-aliases=\"\" data-emoji=\"older_man\" data-unicode-name=\"1F474\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F475\" title=\"older_woman\" data-aliases=\":grandma:\" data-emoji=\"older_woman\" data-unicode-name=\"1F475\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F450\" title=\"open_hands\" data-aliases=\"\" data-emoji=\"open_hands\" data-unicode-name=\"1F450\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62E\" title=\"open_mouth\" data-aliases=\"\" data-emoji=\"open_mouth\" data-unicode-name=\"1F62E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F614\" title=\"pensive\" data-aliases=\"\" data-emoji=\"pensive\" data-unicode-name=\"1F614\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F623\" title=\"persevere\" data-aliases=\"\" data-emoji=\"persevere\" data-unicode-name=\"1F623\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64D\" title=\"person_frowning\" data-aliases=\"\" data-emoji=\"person_frowning\" data-unicode-name=\"1F64D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F471\" title=\"person_with_blond_hair\" data-aliases=\"\" data-emoji=\"person_with_blond_hair\" data-unicode-name=\"1F471\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64E\" title=\"person_with_pouting_face\" data-aliases=\"\" data-emoji=\"person_with_pouting_face\" data-unicode-name=\"1F64E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F447\" title=\"point_down\" data-aliases=\"\" data-emoji=\"point_down\" data-unicode-name=\"1F447\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F448\" title=\"point_left\" data-aliases=\"\" data-emoji=\"point_left\" data-unicode-name=\"1F448\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F449\" title=\"point_right\" data-aliases=\"\" data-emoji=\"point_right\" data-unicode-name=\"1F449\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-261D\" title=\"point_up\" data-aliases=\"\" data-emoji=\"point_up\" data-unicode-name=\"261D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F446\" title=\"point_up_2\" data-aliases=\"\" data-emoji=\"point_up_2\" data-unicode-name=\"1F446\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A9\" title=\"poop\" data-aliases=\":shit: :hankey: :poo:\" data-emoji=\"poop\" data-unicode-name=\"1F4A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45D\" title=\"pouch\" data-aliases=\"\" data-emoji=\"pouch\" data-unicode-name=\"1F45D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63E\" title=\"pouting_cat\" data-aliases=\"\" data-emoji=\"pouting_cat\" data-unicode-name=\"1F63E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64F\" title=\"pray\" data-aliases=\"\" data-emoji=\"pray\" data-unicode-name=\"1F64F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F478\" title=\"princess\" data-aliases=\"\" data-emoji=\"princess\" data-unicode-name=\"1F478\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44A\" title=\"punch\" data-aliases=\"\" data-emoji=\"punch\" data-unicode-name=\"1F44A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49C\" title=\"purple_heart\" data-aliases=\"\" data-emoji=\"purple_heart\" data-unicode-name=\"1F49C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45B\" title=\"purse\" data-aliases=\"\" data-emoji=\"purse\" data-unicode-name=\"1F45B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F621\" title=\"rage\" data-aliases=\"\" data-emoji=\"rage\" data-unicode-name=\"1F621\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270B\" title=\"raised_hand\" data-aliases=\"\" data-emoji=\"raised_hand\" data-unicode-name=\"270B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64C\" title=\"raised_hands\" data-aliases=\"\" data-emoji=\"raised_hands\" data-unicode-name=\"1F64C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64B\" title=\"raising_hand\" data-aliases=\"\" data-emoji=\"raising_hand\" data-unicode-name=\"1F64B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-263A\" title=\"relaxed\" data-aliases=\"\" data-emoji=\"relaxed\" data-unicode-name=\"263A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60C\" title=\"relieved\" data-aliases=\"\" data-emoji=\"relieved\" data-unicode-name=\"1F60C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49E\" title=\"revolving_hearts\" data-aliases=\"\" data-emoji=\"revolving_hearts\" data-unicode-name=\"1F49E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F380\" title=\"ribbon\" data-aliases=\"\" data-emoji=\"ribbon\" data-unicode-name=\"1F380\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F48D\" title=\"ring\" data-aliases=\"\" data-emoji=\"ring\" data-unicode-name=\"1F48D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3C3\" title=\"runner\" data-aliases=\"\" data-emoji=\"runner\" data-unicode-name=\"1F3C3\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3BD\" title=\"running_shirt_with_sash\" data-aliases=\"\" data-emoji=\"running_shirt_with_sash\" data-unicode-name=\"1F3BD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F461\" title=\"sandal\" data-aliases=\"\" data-emoji=\"sandal\" data-unicode-name=\"1F461\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F631\" title=\"scream\" data-aliases=\"\" data-emoji=\"scream\" data-unicode-name=\"1F631\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F640\" title=\"scream_cat\" data-aliases=\"\" data-emoji=\"scream_cat\" data-unicode-name=\"1F640\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F648\" title=\"see_no_evil\" data-aliases=\"\" data-emoji=\"see_no_evil\" data-unicode-name=\"1F648\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F455\" title=\"shirt\" data-aliases=\"\" data-emoji=\"shirt\" data-unicode-name=\"1F455\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F480\" title=\"skull\" data-aliases=\":skeleton:\" data-emoji=\"skull\" data-unicode-name=\"1F480\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F634\" title=\"sleeping\" data-aliases=\"\" data-emoji=\"sleeping\" data-unicode-name=\"1F634\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62A\" title=\"sleepy\" data-aliases=\"\" data-emoji=\"sleepy\" data-unicode-name=\"1F62A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F604\" title=\"smile\" data-aliases=\"\" data-emoji=\"smile\" data-unicode-name=\"1F604\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F638\" title=\"smile_cat\" data-aliases=\"\" data-emoji=\"smile_cat\" data-unicode-name=\"1F638\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F603\" title=\"smiley\" data-aliases=\"\" data-emoji=\"smiley\" data-unicode-name=\"1F603\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63A\" title=\"smiley_cat\" data-aliases=\"\" data-emoji=\"smiley_cat\" data-unicode-name=\"1F63A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F608\" title=\"smiling_imp\" data-aliases=\"\" data-emoji=\"smiling_imp\" data-unicode-name=\"1F608\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60F\" title=\"smirk\" data-aliases=\"\" data-emoji=\"smirk\" data-unicode-name=\"1F60F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F63C\" title=\"smirk_cat\" data-aliases=\"\" data-emoji=\"smirk_cat\" data-unicode-name=\"1F63C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62D\" title=\"sob\" data-aliases=\"\" data-emoji=\"sob\" data-unicode-name=\"1F62D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-2728\" title=\"sparkles\" data-aliases=\"\" data-emoji=\"sparkles\" data-unicode-name=\"2728\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F496\" title=\"sparkling_heart\" data-aliases=\"\" data-emoji=\"sparkling_heart\" data-unicode-name=\"1F496\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F64A\" title=\"speak_no_evil\" data-aliases=\"\" data-emoji=\"speak_no_evil\" data-unicode-name=\"1F64A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AC\" title=\"speech_balloon\" data-aliases=\"\" data-emoji=\"speech_balloon\" data-unicode-name=\"1F4AC\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F31F\" title=\"star2\" data-aliases=\"\" data-emoji=\"star2\" data-unicode-name=\"1F31F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61B\" title=\"stuck_out_tongue\" data-aliases=\"\" data-emoji=\"stuck_out_tongue\" data-unicode-name=\"1F61B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61D\" title=\"stuck_out_tongue_closed_eyes\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_closed_eyes\" data-unicode-name=\"1F61D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61C\" title=\"stuck_out_tongue_winking_eye\" data-aliases=\"\" data-emoji=\"stuck_out_tongue_winking_eye\" data-unicode-name=\"1F61C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60E\" title=\"sunglasses\" data-aliases=\"\" data-emoji=\"sunglasses\" data-unicode-name=\"1F60E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F613\" title=\"sweat\" data-aliases=\"\" data-emoji=\"sweat\" data-unicode-name=\"1F613\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A6\" title=\"sweat_drops\" data-aliases=\"\" data-emoji=\"sweat_drops\" data-unicode-name=\"1F4A6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F605\" title=\"sweat_smile\" data-aliases=\"\" data-emoji=\"sweat_smile\" data-unicode-name=\"1F605\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4AD\" title=\"thought_balloon\" data-aliases=\"\" data-emoji=\"thought_balloon\" data-unicode-name=\"1F4AD\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44E\" title=\"thumbsdown\" data-aliases=\":-1:\" data-emoji=\"thumbsdown\" data-unicode-name=\"1F44E\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44D\" title=\"thumbsup\" data-aliases=\":+1:\" data-emoji=\"thumbsup\" data-unicode-name=\"1F44D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F62B\" title=\"tired_face\" data-aliases=\"\" data-emoji=\"tired_face\" data-unicode-name=\"1F62B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F445\" title=\"tongue\" data-aliases=\"\" data-emoji=\"tongue\" data-unicode-name=\"1F445\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F3A9\" title=\"tophat\" data-aliases=\"\" data-emoji=\"tophat\" data-unicode-name=\"1F3A9\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F624\" title=\"triumph\" data-aliases=\"\" data-emoji=\"triumph\" data-unicode-name=\"1F624\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F495\" title=\"two_hearts\" data-aliases=\"\" data-emoji=\"two_hearts\" data-unicode-name=\"1F495\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46C\" title=\"two_men_holding_hands\" data-aliases=\"\" data-emoji=\"two_men_holding_hands\" data-unicode-name=\"1F46C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F46D\" title=\"two_women_holding_hands\" data-aliases=\"\" data-emoji=\"two_women_holding_hands\" data-unicode-name=\"1F46D\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F612\" title=\"unamused\" data-aliases=\"\" data-emoji=\"unamused\" data-unicode-name=\"1F612\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-270C\" title=\"v\" data-aliases=\"\" data-emoji=\"v\" data-unicode-name=\"270C\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F6B6\" title=\"walking\" data-aliases=\"\" data-emoji=\"walking\" data-unicode-name=\"1F6B6\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F44B\" title=\"wave\" data-aliases=\"\" data-emoji=\"wave\" data-unicode-name=\"1F44B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F629\" title=\"weary\" data-aliases=\"\" data-emoji=\"weary\" data-unicode-name=\"1F629\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F609\" title=\"wink\" data-aliases=\"\" data-emoji=\"wink\" data-unicode-name=\"1F609\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F469\" title=\"woman\" data-aliases=\"\" data-emoji=\"woman\" data-unicode-name=\"1F469\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F45A\" title=\"womans_clothes\" data-aliases=\"\" data-emoji=\"womans_clothes\" data-unicode-name=\"1F45A\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F452\" title=\"womans_hat\" data-aliases=\"\" data-emoji=\"womans_hat\" data-unicode-name=\"1F452\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F61F\" title=\"worried\" data-aliases=\"\" data-emoji=\"worried\" data-unicode-name=\"1F61F\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F49B\" title=\"yellow_heart\" data-aliases=\"\" data-emoji=\"yellow_heart\" data-unicode-name=\"1F49B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F60B\" title=\"yum\" data-aliases=\"\" data-emoji=\"yum\" data-unicode-name=\"1F60B\"></div>\n </button>\n </li>\n <li class='pull-left text-center emoji-menu-list-item'>\n <button class='emoji-menu-btn text-center js-emoji-btn' type='button'>\n <div class=\"icon emoji-icon emoji-1F4A4\" title=\"zzz\" data-aliases=\"\" data-emoji=\"zzz\" data-unicode-name=\"1F4A4\"></div>\n </button>\n </li>\n </ul>\n </div>\n</div>";
diff --git a/spec/javascripts/fixtures/header.html.haml b/spec/javascripts/fixtures/header.html.haml
new file mode 100644
index 00000000000..4db2ef604de
--- /dev/null
+++ b/spec/javascripts/fixtures/header.html.haml
@@ -0,0 +1,35 @@
+%header.navbar.navbar-fixed-top.navbar-gitlab.nav_header_class
+ .container-fluid
+ .header-content
+ %button.side-nav-toggle
+ %span.sr-only
+ Toggle navigation
+ %i.fa.fa-bars
+ %button.navbar-toggle
+ %span.sr-only
+ Toggle navigation
+ %i.fa.fa-ellipsis-v
+ .navbar-collapse.collapse
+ %ui.nav.navbar-nav
+ %li.hidden-sm.hidden-xs
+ %li.visible-sm.visible-xs
+ %li
+ %a
+ %i.fa.fa-bell.fa-fw
+ %span.badge.todos-pending-count
+ %li
+ %a
+ %i.fa.fa-plus.fa-fw
+ %li.header-user.dropdown
+ %a
+ %img
+ %span.caret
+ .dropdown-menu-nav
+ .dropdown-menu-align-right
+ %ul
+ %li
+ %a.profile-link
+ %li
+ %a
+ %li.divider
+ %li.sign-out-link
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
new file mode 100644
index 00000000000..d95eb851421
--- /dev/null
+++ b/spec/javascripts/fixtures/issues.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:project) { create(:project_empty_repo) }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('issues/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'issues/open-issue.html.raw' do |example|
+ render_issue(example.description, create(:issue, project: project))
+ end
+
+ it 'issues/closed-issue.html.raw' do |example|
+ render_issue(example.description, create(:closed_issue, project: project))
+ end
+
+ it 'issues/issue-with-task-list.html.raw' do |example|
+ issue = create(:issue, project: project)
+ issue.update(description: '- [ ] Task List Item')
+ render_issue(example.description, issue)
+ end
+
+ private
+
+ def render_issue(fixture_file_name, issue)
+ get :show,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: issue.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, fixture_file_name)
+ end
+end
diff --git a/spec/javascripts/fixtures/issues_show.html.haml b/spec/javascripts/fixtures/issues_show.html.haml
deleted file mode 100644
index 06c2ab1e823..00000000000
--- a/spec/javascripts/fixtures/issues_show.html.haml
+++ /dev/null
@@ -1,23 +0,0 @@
-:css
- .hidden { display: none !important; }
-
-.flash-container.flash-container-page
- .flash-alert
- .flash-notice
-
-.status-box.status-box-open Open
-.status-box.status-box-closed.hidden Closed
-%a.btn-close{"href" => "http://gitlab.com/issues/6/close"} Close
-%a.btn-reopen.hidden{"href" => "http://gitlab.com/issues/6/reopen"} Reopen
-
-.detail-page-description
- .description.js-task-list-container
- .wiki
- %ul.task-list
- %li.task-list-item
- %input.task-list-item-checkbox{type: 'checkbox'}
- Task List Item
- %textarea.js-task-list-field
- \- [ ] Task List Item
-
-%form.js-issuable-update{action: '/foo'}
diff --git a/spec/javascripts/fixtures/right_sidebar.html.haml b/spec/javascripts/fixtures/right_sidebar.html.haml
index 95efaff4b69..d48b77cf0ce 100644
--- a/spec/javascripts/fixtures/right_sidebar.html.haml
+++ b/spec/javascripts/fixtures/right_sidebar.html.haml
@@ -5,6 +5,10 @@
%div.block.issuable-sidebar-header
%a.gutter-toggle.pull-right.js-sidebar-toggle
%i.fa.fa-angle-double-left
+ %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: "1", issuable_type: "issue", url: "/todos" }}
+ %span.js-issuable-todo-text
+ Add Todo
+ %i.fa.fa-spin.fa-spinner.js-issuable-todo-loading.hidden
%form.issuable-context-form
%div.block.labels
diff --git a/spec/javascripts/fixtures/todos.json b/spec/javascripts/fixtures/todos.json
new file mode 100644
index 00000000000..62c2387d515
--- /dev/null
+++ b/spec/javascripts/fixtures/todos.json
@@ -0,0 +1,4 @@
+{
+ "count": 1,
+ "delete_path": "/dashboard/todos/1"
+} \ No newline at end of file
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
index b529ea6458d..8ba238018cd 100644
--- a/spec/javascripts/gl_dropdown_spec.js.es6
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require jquery */
/*= require gl_dropdown */
/*= require turbolinks */
@@ -6,6 +7,7 @@
(() => {
const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const SEARCH_INPUT_SELECTOR = '.dropdown-input-field';
const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
@@ -16,6 +18,8 @@
ESC: 27
};
+ let remoteCallback;
+
let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
i = i || 0;
if (!i) direction = direction.toUpperCase();
@@ -32,18 +36,19 @@
}
};
+ let remoteMock = function remoteMock(data, term, callback) {
+ remoteCallback = callback.bind({}, data);
+ }
+
describe('Dropdown', function describeDropdown() {
fixture.preload('gl_dropdown.html');
fixture.preload('projects.json');
- beforeEach(() => {
- fixture.load('gl_dropdown.html');
- this.dropdownContainerElement = $('.dropdown.inline');
- this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
- this.projectsData = fixture.load('projects.json')[0];
+ function initDropDown(hasRemote, isFilterable) {
this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
selectable: true,
- data: this.projectsData,
+ filterable: isFilterable,
+ data: hasRemote ? remoteMock.bind({}, this.projectsData) : this.projectsData,
text: (project) => {
(project.name_with_namespace || project.name);
},
@@ -51,6 +56,13 @@
project.id;
}
});
+ }
+
+ beforeEach(() => {
+ fixture.load('gl_dropdown.html');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.$dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = fixture.load('projects.json')[0];
});
afterEach(() => {
@@ -59,6 +71,7 @@
});
it('should open on click', () => {
+ initDropDown.call(this, false);
expect(this.dropdownContainerElement).not.toHaveClass('open');
this.dropdownButtonElement.click();
expect(this.dropdownContainerElement).toHaveClass('open');
@@ -66,26 +79,27 @@
describe('that is open', () => {
beforeEach(() => {
+ initDropDown.call(this, false, false);
this.dropdownButtonElement.click();
});
it('should select a following item on DOWN keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
navigateWithKeys('down', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
});
it('should select a previous item on UP keypress', () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(0);
navigateWithKeys('down', (this.projectsData.length - 1), () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
navigateWithKeys('up', randomIndex, () => {
- expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
- expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ expect($(FOCUSED_ITEM_SELECTOR, this.$dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.$dropdownMenuElement)).toHaveClass('is-focused');
});
});
});
@@ -97,7 +111,7 @@
spyOn(Turbolinks, 'visit').and.stub();
navigateWithKeys('enter', null, () => {
expect(this.dropdownContainerElement).not.toHaveClass('open');
- let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
+ let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.$dropdownMenuElement);
expect(link).toHaveClass('is-active');
let linkedLocation = link.attr('href');
if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
@@ -115,5 +129,42 @@
expect(this.dropdownContainerElement).not.toHaveClass('open');
});
});
+
+ describe('opened and waiting for a remote callback', () => {
+ beforeEach(() => {
+ initDropDown.call(this, true, true);
+ this.dropdownButtonElement.click();
+ });
+
+ it('should not focus search input while remote task is not complete', ()=> {
+ expect($(document.activeElement)).not.toEqual($(SEARCH_INPUT_SELECTOR));
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus search input after remote task is complete', ()=> {
+ remoteCallback();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+
+ it('should focus on input when opening for the second time', ()=> {
+ remoteCallback();
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ this.dropdownButtonElement.click();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
+
+ describe('input focus with array data', () => {
+ it('should focus input when passing array data to drop down', ()=> {
+ initDropDown.call(this, false, true);
+ this.dropdownButtonElement.click();
+ expect($(document.activeElement)).toEqual($(SEARCH_INPUT_SELECTOR));
+ });
+ });
});
})();
diff --git a/spec/javascripts/gl_field_errors_spec.js.es6 b/spec/javascripts/gl_field_errors_spec.js.es6
index da9259edd78..4bdd72800ea 100644
--- a/spec/javascripts/gl_field_errors_spec.js.es6
+++ b/spec/javascripts/gl_field_errors_spec.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require jquery
//= require gl_field_errors
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index d5401fbb0d1..8c66c45ba79 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require graphs/stat_graph_contributors_graph
describe("ContributorsGraph", function () {
diff --git a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
index 56970e22e34..920e4ee0892 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_util_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require graphs/stat_graph_contributors_util
describe("ContributorsStatGraphUtil", function () {
diff --git a/spec/javascripts/graphs/stat_graph_spec.js b/spec/javascripts/graphs/stat_graph_spec.js
index 4b05d401a42..ae2821ecad9 100644
--- a/spec/javascripts/graphs/stat_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require graphs/stat_graph
describe("StatGraph", function () {
diff --git a/spec/javascripts/header_spec.js b/spec/javascripts/header_spec.js
new file mode 100644
index 00000000000..9a859655d8b
--- /dev/null
+++ b/spec/javascripts/header_spec.js
@@ -0,0 +1,55 @@
+/* eslint-disable */
+/*= require header */
+/*= require lib/utils/text_utility */
+/*= require jquery */
+
+(function() {
+
+ describe('Header', function() {
+ var todosPendingCount = '.todos-pending-count';
+ var fixtureTemplate = 'header.html';
+
+ function isTodosCountHidden() {
+ return $(todosPendingCount).hasClass('hidden');
+ }
+
+ function triggerToggle(newCount) {
+ $(document).trigger('todo:toggle', newCount);
+ }
+
+ fixture.preload(fixtureTemplate);
+ beforeEach(function() {
+ fixture.load(fixtureTemplate);
+ });
+
+ it('should update todos-pending-count after receiving the todo:toggle event', function() {
+ triggerToggle(5);
+ expect($(todosPendingCount).text()).toEqual('5');
+ });
+
+ it('should hide todos-pending-count when it is 0', function() {
+ triggerToggle(0);
+ expect(isTodosCountHidden()).toEqual(true);
+ });
+
+ it('should show todos-pending-count when it is more than 0', function() {
+ triggerToggle(10);
+ expect(isTodosCountHidden()).toEqual(false);
+ });
+
+ describe('when todos-pending-count is 1000', function() {
+ beforeEach(function() {
+ triggerToggle(1000);
+ });
+
+ it('should show todos-pending-count', function() {
+ expect(isTodosCountHidden()).toEqual(false);
+ });
+
+ it('should add delimiter to todos-pending-count', function() {
+ expect($(todosPendingCount).text()).toEqual('1,000');
+ });
+ });
+ });
+
+}).call(this);
diff --git a/spec/javascripts/issue_spec.js b/spec/javascripts/issue_spec.js
index 33690c7a5f3..949114185cf 100644
--- a/spec/javascripts/issue_spec.js
+++ b/spec/javascripts/issue_spec.js
@@ -1,118 +1,163 @@
+/* eslint-disable */
/*= require lib/utils/text_utility */
/*= require issue */
(function() {
+ var INVALID_URL = 'http://goesnowhere.nothing/whereami';
+ var $boxClosed, $boxOpen, $btnClose, $btnReopen;
+
+ fixture.preload('issues/closed-issue.html');
+ fixture.preload('issues/issue-with-task-list.html');
+ fixture.preload('issues/open-issue.html');
+
+ function expectErrorMessage() {
+ var $flashMessage = $('div.flash-alert');
+ expect($flashMessage).toExist();
+ expect($flashMessage).toBeVisible();
+ expect($flashMessage).toHaveText('Unable to update this issue at this time.');
+ }
+
+ function expectIssueState(isIssueOpen) {
+ expectVisibility($boxClosed, !isIssueOpen);
+ expectVisibility($boxOpen, isIssueOpen);
+
+ expectVisibility($btnClose, isIssueOpen);
+ expectVisibility($btnReopen, !isIssueOpen);
+ }
+
+ function expectPendingRequest(req, $triggeredButton) {
+ expect(req.type).toBe('PUT');
+ expect(req.url).toBe($triggeredButton.attr('href'));
+ expect($triggeredButton).toHaveProp('disabled', true);
+ }
+
+ function expectVisibility($element, shouldBeVisible) {
+ if (shouldBeVisible) {
+ expect($element).not.toHaveClass('hidden');
+ } else {
+ expect($element).toHaveClass('hidden');
+ }
+ }
+
+ function findElements() {
+ $boxClosed = $('div.status-box-closed');
+ expect($boxClosed).toExist();
+ expect($boxClosed).toHaveText('Closed');
+
+ $boxOpen = $('div.status-box-open');
+ expect($boxOpen).toExist();
+ expect($boxOpen).toHaveText('Open');
+
+ $btnClose = $('.btn-close.btn-grouped');
+ expect($btnClose).toExist();
+ expect($btnClose).toHaveText('Close issue');
+
+ $btnReopen = $('.btn-reopen.btn-grouped');
+ expect($btnReopen).toExist();
+ expect($btnReopen).toHaveText('Reopen issue');
+ }
+
describe('Issue', function() {
- return describe('task lists', function() {
- fixture.preload('issues_show.html');
+ describe('task lists', function() {
+ fixture.load('issues/issue-with-task-list.html');
beforeEach(function() {
- fixture.load('issues_show.html');
- return this.issue = new Issue();
+ this.issue = new Issue();
});
+
it('modifies the Markdown field', function() {
spyOn(jQuery, 'ajax').and.stub();
$('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 an ajax request on tasklist:changed', function() {
+
+ it('submits an ajax request on tasklist:changed', function() {
spyOn(jQuery, 'ajax').and.callFake(function(req) {
expect(req.type).toBe('PATCH');
- expect(req.url).toBe('/foo');
- return expect(req.data.issue.description).not.toBe(null);
+ expect(req.url).toBe('https://fixture.invalid/namespace3/project3/issues/1.json');
+ expect(req.data.issue.description).not.toBe(null);
});
- return $('.js-task-list-field').trigger('tasklist:changed');
+
+ $('.js-task-list-field').trigger('tasklist:changed');
});
});
});
- describe('reopen/close issue', function() {
- fixture.preload('issues_show.html');
+ describe('close issue', function() {
beforeEach(function() {
- fixture.load('issues_show.html');
- return this.issue = new Issue();
+ fixture.load('issues/open-issue.html');
+ findElements();
+ this.issue = new Issue();
+
+ expectIssueState(true);
});
+
it('closes an issue', function() {
- var $btnClose, $btnReopen;
spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PUT');
- expect(req.url).toBe('http://gitlab.com/issues/6/close');
- return req.success({
+ expectPendingRequest(req, $btnClose);
+ req.success({
id: 34
});
});
- $btnClose = $('a.btn-close');
- $btnReopen = $('a.btn-reopen');
- expect($btnReopen).toBeHidden();
- expect($btnClose.text()).toBe('Close');
- expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+
$btnClose.trigger('click');
- expect($btnReopen).toBeVisible();
- expect($btnClose).toBeHidden();
- expect($('div.status-box-closed')).toBeVisible();
- return expect($('div.status-box-open')).toBeHidden();
+
+ expectIssueState(false);
+ expect($btnClose).toHaveProp('disabled', false);
});
+
it('fails to close an issue with success:false', function() {
- var $btnClose, $btnReopen;
spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PUT');
- expect(req.url).toBe('http://goesnowhere.nothing/whereami');
- return req.success({
+ expectPendingRequest(req, $btnClose);
+ req.success({
saved: false
});
});
- $btnClose = $('a.btn-close');
- $btnReopen = $('a.btn-reopen');
- $btnClose.attr('href', 'http://goesnowhere.nothing/whereami');
- expect($btnReopen).toBeHidden();
- expect($btnClose.text()).toBe('Close');
- expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+
+ $btnClose.attr('href', INVALID_URL);
$btnClose.trigger('click');
- expect($btnReopen).toBeHidden();
- expect($btnClose).toBeVisible();
- expect($('div.status-box-closed')).toBeHidden();
- expect($('div.status-box-open')).toBeVisible();
- expect($('div.flash-alert')).toBeVisible();
- return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.');
+
+ expectIssueState(true);
+ expect($btnClose).toHaveProp('disabled', false);
+ expectErrorMessage();
});
+
it('fails to closes an issue with HTTP error', function() {
- var $btnClose, $btnReopen;
spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PUT');
- expect(req.url).toBe('http://goesnowhere.nothing/whereami');
- return req.error();
+ expectPendingRequest(req, $btnClose);
+ req.error();
});
- $btnClose = $('a.btn-close');
- $btnReopen = $('a.btn-reopen');
- $btnClose.attr('href', 'http://goesnowhere.nothing/whereami');
- expect($btnReopen).toBeHidden();
- expect($btnClose.text()).toBe('Close');
- expect(typeof $btnClose.prop('disabled')).toBe('undefined');
+
+ $btnClose.attr('href', INVALID_URL);
$btnClose.trigger('click');
- expect($btnReopen).toBeHidden();
- expect($btnClose).toBeVisible();
- expect($('div.status-box-closed')).toBeHidden();
- expect($('div.status-box-open')).toBeVisible();
- expect($('div.flash-alert')).toBeVisible();
- return expect($('div.flash-alert').text()).toBe('Unable to update this issue at this time.');
+
+ expectIssueState(true);
+ expect($btnClose).toHaveProp('disabled', true);
+ expectErrorMessage();
});
- return it('reopens an issue', function() {
- var $btnClose, $btnReopen;
+ });
+
+ describe('reopen issue', function() {
+ beforeEach(function() {
+ fixture.load('issues/closed-issue.html');
+ findElements();
+ this.issue = new Issue();
+
+ expectIssueState(false);
+ });
+
+ it('reopens an issue', function() {
spyOn(jQuery, 'ajax').and.callFake(function(req) {
- expect(req.type).toBe('PUT');
- expect(req.url).toBe('http://gitlab.com/issues/6/reopen');
- return req.success({
+ expectPendingRequest(req, $btnReopen);
+ req.success({
id: 34
});
});
- $btnClose = $('a.btn-close');
- $btnReopen = $('a.btn-reopen');
- expect($btnReopen.text()).toBe('Reopen');
+
$btnReopen.trigger('click');
- expect($btnReopen).toBeHidden();
- expect($btnClose).toBeVisible();
- expect($('div.status-box-open')).toBeVisible();
- return expect($('div.status-box-closed')).toBeHidden();
+
+ expectIssueState(true);
+ expect($btnReopen).toHaveProp('disabled', false);
});
});
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 1ad6f612210..49687048eb5 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -1,3 +1,4 @@
+/* eslint-disable */
//= require lib/utils/type_utility
//= require jquery
//= require bootstrap
diff --git a/spec/javascripts/line_highlighter_spec.js b/spec/javascripts/line_highlighter_spec.js
index e2789571607..e0192a2d624 100644
--- a/spec/javascripts/line_highlighter_spec.js
+++ b/spec/javascripts/line_highlighter_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require line_highlighter */
diff --git a/spec/javascripts/merge_request_spec.js b/spec/javascripts/merge_request_spec.js
index 61830d267a9..83d279ab414 100644
--- a/spec/javascripts/merge_request_spec.js
+++ b/spec/javascripts/merge_request_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require merge_request */
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 96ee5235acf..6a53c6aa6ac 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require merge_request_tabs */
//= require breakpoints
diff --git a/spec/javascripts/merge_request_widget_spec.js b/spec/javascripts/merge_request_widget_spec.js
index c9175e2b704..1e2072f370a 100644
--- a/spec/javascripts/merge_request_widget_spec.js
+++ b/spec/javascripts/merge_request_widget_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require merge_request_widget */
/*= require lib/utils/jquery.timeago.js */
diff --git a/spec/javascripts/new_branch_spec.js b/spec/javascripts/new_branch_spec.js
index f09596bd36d..c092424ec32 100644
--- a/spec/javascripts/new_branch_spec.js
+++ b/spec/javascripts/new_branch_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require jquery-ui/autocomplete */
/*= require new_branch_form */
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index a588f403dd5..2e3a4b66e2d 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require notes */
/*= require autosize */
/*= require gl_form */
diff --git a/spec/javascripts/project_title_spec.js b/spec/javascripts/project_title_spec.js
index 51eb12b41d4..1963857bba3 100644
--- a/spec/javascripts/project_title_spec.js
+++ b/spec/javascripts/project_title_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require bootstrap */
/*= require select2 */
diff --git a/spec/javascripts/right_sidebar_spec.js b/spec/javascripts/right_sidebar_spec.js
index c937a4706f7..ef03d1147de 100644
--- a/spec/javascripts/right_sidebar_spec.js
+++ b/spec/javascripts/right_sidebar_spec.js
@@ -1,7 +1,10 @@
+/* eslint-disable */
/*= require right_sidebar */
/*= require jquery */
-/*= require jquery.cookie */
+/*= require js.cookie */
+
+/*= require extensions/jquery.js */
(function() {
var $aside, $icon, $labelsIcon, $page, $toggle, assertSidebarState;
@@ -55,12 +58,27 @@
$labelsIcon.click();
return assertSidebarState('expanded');
});
- return it('should collapse when the icon arrow clicked while it is floating on page', function() {
+ it('should collapse when the icon arrow clicked while it is floating on page', function() {
$labelsIcon.click();
assertSidebarState('expanded');
$toggle.click();
return assertSidebarState('collapsed');
});
+
+ it('should broadcast todo:toggle event when add todo clicked', function() {
+ spyOn(jQuery, 'ajax').and.callFake(function() {
+ var d = $.Deferred();
+ var response = fixture.load('todos.json');
+ d.resolve(response);
+ return d.promise();
+ });
+
+ var todoToggleSpy = spyOnEvent(document, 'todo:toggle');
+
+ $('.js-issuable-todo').click();
+
+ expect(todoToggleSpy.calls.count()).toEqual(1);
+ })
});
}).call(this);
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 333128782a2..29080804960 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require gl_dropdown */
/*= require search_autocomplete */
diff --git a/spec/javascripts/shortcuts_issuable_spec.js b/spec/javascripts/shortcuts_issuable_spec.js
index 04ccf246052..1f36a048153 100644
--- a/spec/javascripts/shortcuts_issuable_spec.js
+++ b/spec/javascripts/shortcuts_issuable_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require shortcuts_issuable */
diff --git a/spec/javascripts/spec_helper.js b/spec/javascripts/spec_helper.js
index 8801c297887..9cb8243ee2c 100644
--- a/spec/javascripts/spec_helper.js
+++ b/spec/javascripts/spec_helper.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
// PhantomJS (Teaspoons default driver) doesn't have support for
// Function.prototype.bind, which has caused confusion. Use this polyfill to
// avoid the confusion.
@@ -27,7 +28,7 @@
// 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
+// _spec.{js,js.es6}. 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
diff --git a/spec/javascripts/syntax_highlight_spec.js b/spec/javascripts/syntax_highlight_spec.js
index 4e5dd1e59bf..498f0f06797 100644
--- a/spec/javascripts/syntax_highlight_spec.js
+++ b/spec/javascripts/syntax_highlight_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require syntax_highlight */
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 784b43d4846..024a91f0a80 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require u2f/authenticate */
/*= require u2f/util */
diff --git a/spec/javascripts/u2f/mock_u2f_device.js b/spec/javascripts/u2f/mock_u2f_device.js
index ca91a716ba3..ad133682fb1 100644
--- a/spec/javascripts/u2f/mock_u2f_device.js
+++ b/spec/javascripts/u2f/mock_u2f_device.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
(function() {
var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index 01d6b7a8961..abea76f622f 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require u2f/register */
/*= require u2f/util */
diff --git a/spec/javascripts/zen_mode_spec.js b/spec/javascripts/zen_mode_spec.js
index 0c1266800d7..65b6e3dce33 100644
--- a/spec/javascripts/zen_mode_spec.js
+++ b/spec/javascripts/zen_mode_spec.js
@@ -1,3 +1,4 @@
+/* eslint-disable */
/*= require zen_mode */
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index 2f9343fadaf..fbf7a461fa5 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -8,6 +8,8 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
end
shared_examples_for "external issue tracker" do
+ it_behaves_like 'a reference containing an element node'
+
it 'requires project context' do
expect { described_class.call('') }.to raise_error(ArgumentError, /:project/)
end
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index a2025672ad9..8f0b2db3e8e 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -22,6 +22,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
context 'internal reference' do
+ it_behaves_like 'a reference containing an element node'
+
let(:reference) { issue.to_reference }
it 'ignores valid references when using non-default tracker' do
@@ -83,6 +85,20 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(link.attr('data-issue')).to eq issue.id.to_s
end
+ it 'includes a data-original attribute' do
+ doc = reference_filter("See #{reference}")
+ link = doc.css('a').first
+
+ expect(link).to have_attribute('data-original')
+ expect(link.attr('data-original')).to eq reference
+ end
+
+ it 'does not escape the data-original attribute' do
+ inner_html = 'element <code>node</code> inside'
+ doc = reference_filter(%{<a href="#{reference}">#{inner_html}</a>})
+ expect(doc.children.first.attr('data-original')).to eq inner_html
+ end
+
it 'supports an :only_path context' do
doc = reference_filter("Issue #{reference}", only_path: true)
link = doc.css('a').first.attr('href')
@@ -101,6 +117,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
context 'cross-project reference' do
+ it_behaves_like 'a reference containing an element node'
+
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
@@ -141,6 +159,8 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
context 'cross-project URL reference' do
+ it_behaves_like 'a reference containing an element node'
+
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
@@ -160,39 +180,45 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
end
context 'cross-project reference in link href' do
+ it_behaves_like 'a reference containing an element node'
+
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
- let(:reference) { %Q{<a href="#{issue.to_reference(project)}">Reference</a>} }
+ let(:reference) { issue.to_reference(project) }
+ let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
it 'links to a valid reference' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{reference_link}")
expect(doc.css('a').first.attr('href')).
to eq helper.url_for_issue(issue.iid, project2)
end
it 'links with adjacent text' do
- doc = reference_filter("Fixed (#{reference}.)")
+ doc = reference_filter("Fixed (#{reference_link}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
end
context 'cross-project URL in link href' do
+ it_behaves_like 'a reference containing an element node'
+
let(:namespace) { create(:namespace, name: 'cross-reference') }
let(:project2) { create(:empty_project, :public, namespace: namespace) }
let(:issue) { create(:issue, project: project2) }
- let(:reference) { %Q{<a href="#{helper.url_for_issue(issue.iid, project2) + "#note_123"}">Reference</a>} }
+ let(:reference) { "#{helper.url_for_issue(issue.iid, project2) + "#note_123"}" }
+ let(:reference_link) { %{<a href="#{reference}">Reference</a>} }
it 'links to a valid reference' do
- doc = reference_filter("See #{reference}")
+ doc = reference_filter("See #{reference_link}")
expect(doc.css('a').first.attr('href')).
to eq helper.url_for_issue(issue.iid, project2) + "#note_123"
end
it 'links with adjacent text' do
- doc = reference_filter("Fixed (#{reference}.)")
+ doc = reference_filter("Fixed (#{reference_link}.)")
expect(doc.to_html).to match(/\(<a.+>Reference<\/a>\.\)/)
end
end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 729e77fd43f..5bfeb82e738 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -24,6 +24,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
context 'mentioning @all' do
+ it_behaves_like 'a reference containing an element node'
+
let(:reference) { User.reference_prefix + 'all' }
before do
@@ -60,6 +62,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
context 'mentioning a user' do
+ it_behaves_like 'a reference containing an element node'
+
it 'links to a User' do
doc = reference_filter("Hey #{reference}")
expect(doc.css('a').first.attr('href')).to eq urls.user_url(user)
@@ -89,6 +93,8 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
context 'mentioning a group' do
+ it_behaves_like 'a reference containing an element node'
+
let(:group) { create(:group) }
let(:reference) { group.to_reference }
diff --git a/spec/lib/banzai/pipeline/full_pipeline_spec.rb b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
new file mode 100644
index 00000000000..2501b638774
--- /dev/null
+++ b/spec/lib/banzai/pipeline/full_pipeline_spec.rb
@@ -0,0 +1,28 @@
+require 'rails_helper'
+
+describe Banzai::Pipeline::FullPipeline do
+ describe 'References' do
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ it 'handles markdown inside a reference' do
+ markdown = "[some `code` inside](#{issue.to_reference})"
+ result = described_class.call(markdown, project: project)
+ link_content = result[:output].css('a').inner_html
+ expect(link_content).to eq('some <code>code</code> inside')
+ end
+
+ it 'sanitizes reference HTML' do
+ link_label = '<script>bad things</script>'
+ markdown = "[#{link_label}](#{issue.to_reference})"
+ result = described_class.to_html(markdown, project: project)
+ expect(result).not_to include(link_label)
+ end
+
+ it 'escapes the data-original attribute on a reference' do
+ markdown = %Q{[">bad things](#{issue.to_reference})}
+ result = described_class.to_html(markdown, project: project)
+ expect(result).to include(%{data-original='\"&gt;bad things'})
+ end
+ end
+end
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index 254657a881d..6d2c141e18b 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -6,39 +6,60 @@ describe Banzai::Redactor do
let(:redactor) { described_class.new(project, user) }
describe '#redact' do
- it 'redacts an Array of documents' do
- doc1 = Nokogiri::HTML.
- fragment('<a class="gfm" data-reference-type="issue">foo</a>')
-
- doc2 = Nokogiri::HTML.
- fragment('<a class="gfm" data-reference-type="issue">bar</a>')
-
- expect(redactor).to receive(:nodes_visible_to_user).and_return([])
-
- redacted_data = redactor.redact([doc1, doc2])
-
- expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
- expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0])
- expect(doc1.to_html).to eq('foo')
- expect(doc2.to_html).to eq('bar')
+ context 'when reference not visible to user' do
+ before do
+ expect(redactor).to receive(:nodes_visible_to_user).and_return([])
+ end
+
+ it 'redacts an array of documents' do
+ doc1 = Nokogiri::HTML.
+ fragment('<a class="gfm" data-reference-type="issue">foo</a>')
+
+ doc2 = Nokogiri::HTML.
+ fragment('<a class="gfm" data-reference-type="issue">bar</a>')
+
+ redacted_data = redactor.redact([doc1, doc2])
+
+ expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+ expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([0, 0])
+ expect(doc1.to_html).to eq('foo')
+ expect(doc2.to_html).to eq('bar')
+ end
+
+ it 'replaces redacted reference with inner HTML' do
+ doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue'>foo</a>")
+ redactor.redact([doc])
+ expect(doc.to_html).to eq('foo')
+ end
+
+ context 'when data-original attribute provided' do
+ let(:original_content) { '<code>foo</code>' }
+ it 'replaces redacted reference with original content' do
+ doc = Nokogiri::HTML.fragment("<a class='gfm' data-reference-type='issue' data-original='#{original_content}'>bar</a>")
+ redactor.redact([doc])
+ expect(doc.to_html).to eq(original_content)
+ end
+ end
end
- it 'does not redact an Array of documents' do
- doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
- doc1 = Nokogiri::HTML.fragment(doc1_html)
+ context 'when reference visible to user' do
+ it 'does not redact an array of documents' do
+ doc1_html = '<a class="gfm" data-reference-type="issue">foo</a>'
+ doc1 = Nokogiri::HTML.fragment(doc1_html)
- doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>'
- doc2 = Nokogiri::HTML.fragment(doc2_html)
+ doc2_html = '<a class="gfm" data-reference-type="issue">bar</a>'
+ doc2 = Nokogiri::HTML.fragment(doc2_html)
- nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] }
- expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten)
+ nodes = redactor.document_nodes([doc1, doc2]).map { |x| x[:nodes] }
+ expect(redactor).to receive(:nodes_visible_to_user).and_return(nodes.flatten)
- redacted_data = redactor.redact([doc1, doc2])
+ redacted_data = redactor.redact([doc1, doc2])
- expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
- expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1])
- expect(doc1.to_html).to eq(doc1_html)
- expect(doc2.to_html).to eq(doc2_html)
+ expect(redacted_data.map { |data| data[:document] }).to eq([doc1, doc2])
+ expect(redacted_data.map { |data| data[:visible_reference_count] }).to eq([1, 1])
+ expect(doc1.to_html).to eq(doc1_html)
+ expect(doc2.to_html).to eq(doc2_html)
+ end
end
end
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
index fbdb7ea34ac..6b3bd08b978 100644
--- a/spec/lib/gitlab/exclusive_lease_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -1,21 +1,36 @@
require 'spec_helper'
-describe Gitlab::ExclusiveLease do
- it 'cannot obtain twice before the lease has expired' do
- lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
- expect(lease.try_obtain).to eq(true)
- expect(lease.try_obtain).to eq(false)
- end
+describe Gitlab::ExclusiveLease, type: :redis do
+ let(:unique_key) { SecureRandom.hex(10) }
+
+ describe '#try_obtain' do
+ it 'cannot obtain twice before the lease has expired' do
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
+ expect(lease.try_obtain).to eq(true)
+ expect(lease.try_obtain).to eq(false)
+ end
- it 'can obtain after the lease has expired' do
- timeout = 1
- lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
- lease.try_obtain # start the lease
- sleep(2 * timeout) # lease should have expired now
- expect(lease.try_obtain).to eq(true)
+ it 'can obtain after the lease has expired' do
+ timeout = 1
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: timeout)
+ lease.try_obtain # start the lease
+ sleep(2 * timeout) # lease should have expired now
+ expect(lease.try_obtain).to eq(true)
+ end
end
- def unique_key
- SecureRandom.hex(10)
+ describe '#exists?' do
+ it 'returns true for an existing lease' do
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
+ lease.try_obtain
+
+ expect(lease.exists?).to eq(true)
+ end
+
+ it 'returns false for a lease that does not exist' do
+ lease = Gitlab::ExclusiveLease.new(unique_key, timeout: 3600)
+
+ expect(lease.exists?).to eq(false)
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 1af553f8f03..7478f86bd28 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -2,6 +2,10 @@ require 'spec_helper'
describe Gitlab::GithubImport::Importer, lib: true do
describe '#execute' do
+ before do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+ end
+
context 'when an error occurs' do
let(:project) { create(:project, import_url: 'https://github.com/octocat/Hello-World.git', wiki_access_level: ProjectFeature::DISABLED) }
let(:octocat) { double(id: 123456, login: 'octocat') }
@@ -152,9 +156,9 @@ describe Gitlab::GithubImport::Importer, lib: true do
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 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/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
- { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
+ { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
+ { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :wiki, errors: "Gitlab::Shell::Error" },
{ type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
]
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index feee0f025d8..07a2c316899 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -178,6 +178,7 @@ Ci::Pipeline:
- finished_at
- duration
- user_id
+- lock_version
CommitStatus:
- id
- project_id
@@ -217,6 +218,7 @@ CommitStatus:
- yaml_variables
- queued_at
- token
+- lock_version
Ci::Variable:
- id
- project_id
diff --git a/spec/lib/gitlab/optimistic_locking_spec.rb b/spec/lib/gitlab/optimistic_locking_spec.rb
new file mode 100644
index 00000000000..498dc514c8c
--- /dev/null
+++ b/spec/lib/gitlab/optimistic_locking_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::OptimisticLocking, lib: true do
+ describe '#retry_lock' do
+ let!(:pipeline) { create(:ci_pipeline) }
+ let!(:pipeline2) { Ci::Pipeline.find(pipeline.id) }
+
+ it 'does not reload object if state changes' do
+ expect(pipeline).not_to receive(:reload)
+ expect(pipeline).to receive(:succeed).and_call_original
+
+ described_class.retry_lock(pipeline) do |subject|
+ subject.succeed
+ end
+ end
+
+ it 'retries action if exception is raised' do
+ pipeline.succeed
+
+ expect(pipeline2).to receive(:reload).and_call_original
+ expect(pipeline2).to receive(:drop).twice.and_call_original
+
+ described_class.retry_lock(pipeline2) do |subject|
+ subject.drop
+ end
+ end
+
+ it 'raises exception when too many retries' do
+ expect(pipeline).to receive(:drop).twice.and_call_original
+
+ expect do
+ described_class.retry_lock(pipeline, 1) do |subject|
+ subject.lock_version = 100
+ subject.drop
+ end
+ end.to raise_error(ActiveRecord::StaleObjectError)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index 74ff85e132a..e5406fb2d33 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -116,6 +116,55 @@ describe Gitlab::Redis do
end
end
+ describe '#sentinels' do
+ subject { described_class.new(Rails.env).sentinels }
+
+ context 'when sentinels are defined' do
+ let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+
+ it 'returns an array of hashes with host and port keys' do
+ stub_const("#{described_class}::CONFIG_FILE", config)
+
+ is_expected.to include(host: 'localhost', port: 26380)
+ is_expected.to include(host: 'slave2', port: 26381)
+ end
+ end
+
+ context 'when sentinels are not defined' do
+ let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+
+ it 'returns nil' do
+ stub_const("#{described_class}::CONFIG_FILE", config)
+
+ is_expected.to be_nil
+ end
+ end
+ end
+
+ describe '#sentinels?' do
+ subject { described_class.new(Rails.env).sentinels? }
+
+ context 'when sentinels are defined' do
+ let(:config) { Rails.root.join('spec/fixtures/config/redis_new_format_host.yml') }
+
+ it 'returns true' do
+ stub_const("#{described_class}::CONFIG_FILE", config)
+
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when sentinels are not defined' do
+ let(:config) { Rails.root.join('spec/fixtures/config/redis_old_format_host.yml') }
+
+ it 'returns false' do
+ stub_const("#{described_class}::CONFIG_FILE", config)
+
+ is_expected.to be_falsey
+ end
+ end
+ end
+
describe '#raw_config_hash' do
it 'returns default redis url when no config file is present' do
expect(subject).to receive(:fetch_config) { false }
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
new file mode 100644
index 00000000000..d5d87310874
--- /dev/null
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -0,0 +1,35 @@
+describe Gitlab::Utils, lib: true do
+ def to_boolean(value)
+ described_class.to_boolean(value)
+ end
+
+ describe '.to_boolean' do
+ it 'accepts booleans' do
+ expect(to_boolean(true)).to be(true)
+ expect(to_boolean(false)).to be(false)
+ end
+
+ it 'converts a valid string to a boolean' do
+ expect(to_boolean(true)).to be(true)
+ expect(to_boolean('true')).to be(true)
+ expect(to_boolean('YeS')).to be(true)
+ expect(to_boolean('t')).to be(true)
+ expect(to_boolean('1')).to be(true)
+ expect(to_boolean('ON')).to be(true)
+
+ expect(to_boolean('FaLse')).to be(false)
+ expect(to_boolean('F')).to be(false)
+ expect(to_boolean('NO')).to be(false)
+ expect(to_boolean('n')).to be(false)
+ expect(to_boolean('0')).to be(false)
+ expect(to_boolean('oFF')).to be(false)
+ end
+
+ it 'converts an invalid string to nil' do
+ expect(to_boolean('fals')).to be_nil
+ expect(to_boolean('yeah')).to be_nil
+ expect(to_boolean('')).to be_nil
+ expect(to_boolean(nil)).to be_nil
+ end
+ end
+end
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 65e663be411..71b7628ef10 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -138,32 +138,26 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
- let(:build) { create_build('build1', current, 10) }
- let(:build_b) { create_build('build2', current, 20) }
- let(:build_c) { create_build('build3', current + 50, 10) }
+ let(:build) { create_build('build1', 0) }
+ let(:build_b) { create_build('build2', 0) }
+ let(:build_c) { create_build('build3', 0) }
describe '#duration' do
before do
- pipeline.update(created_at: current)
-
- travel_to(current + 5) do
- pipeline.run
- pipeline.save
- end
-
travel_to(current + 30) do
- build.success
+ build.run!
+ build.success!
+ build_b.run!
+ build_c.run!
end
travel_to(current + 40) do
- build_b.drop
+ build_b.drop!
end
travel_to(current + 70) do
- build_c.success
+ build_c.success!
end
-
- pipeline.drop
end
it 'matches sum of builds duration' do
@@ -455,7 +449,9 @@ describe Ci::Pipeline, models: true do
context 'when all builds succeed' do
before do
build_a.success
- build_b.success
+
+ # We have to reload build_b as this is in next stage and it gets triggered by PipelineProcessWorker
+ build_b.reload.success
end
it 'receives a success event once' do
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 60e4bbc8564..a59d30687f6 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -298,6 +298,20 @@ describe Issue, "Issuable" do
end
end
+ describe '.order_labels_priority' do
+ let(:label_1) { create(:label, title: 'label_1', project: issue.project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: issue.project, priority: 2) }
+
+ subject { Issue.order_labels_priority(excluded_labels: ['label_1']).first.highest_priority }
+
+ before do
+ issue.labels << label_1
+ issue.labels << label_2
+ end
+
+ it { is_expected.to eq(2) }
+ end
+
describe ".with_label" do
let(:project) { create(:project, :public) }
let(:bug) { create(:label, project: project, title: 'bug') }
diff --git a/spec/models/concerns/project_features_compatibility_spec.rb b/spec/models/concerns/project_features_compatibility_spec.rb
index 5363aea4d22..9041690023f 100644
--- a/spec/models/concerns/project_features_compatibility_spec.rb
+++ b/spec/models/concerns/project_features_compatibility_spec.rb
@@ -22,4 +22,18 @@ describe ProjectFeaturesCompatibility do
expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
end
end
+
+ it "converts fields from true to ProjectFeature::ENABLED" do
+ features.each do |feature|
+ project.update_attribute("#{feature}_enabled".to_sym, true)
+ expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::ENABLED)
+ end
+ end
+
+ it "converts fields from false to ProjectFeature::DISABLED" do
+ features.each do |feature|
+ project.update_attribute("#{feature}_enabled".to_sym, false)
+ expect(project.project_feature.public_send("#{feature}_access_level")).to eq(ProjectFeature::DISABLED)
+ end
+ end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index ac862055ebc..47f89f744cb 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -265,4 +265,10 @@ describe Group, models: true do
members
end
+
+ describe '#web_url' do
+ it 'returns the canonical URL' do
+ expect(group.web_url).to include("groups/#{group.name}")
+ end
+ end
end
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index f6b2ec5ae31..68f72f5c86e 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -57,12 +57,12 @@ describe ProjectMember, models: true do
it "creates an expired event when left due to expiry" do
expired = create(:project_member, project: project, expires_at: Time.now - 6.days)
expired.destroy
- expect(Event.first.action).to eq(Event::EXPIRED)
+ expect(Event.recent.first.action).to eq(Event::EXPIRED)
end
it "creates a left event when left due to leave" do
master.destroy
- expect(Event.first.action).to eq(Event::LEFT)
+ expect(Event.recent.first.action).to eq(Event::LEFT)
end
it "destroys itself and delete associated todos" do
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 6ff32aea018..a9f637147d1 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -10,23 +10,15 @@ describe JiraService, models: true do
context 'when service is active' do
before { subject.active = true }
- it { is_expected.to validate_presence_of(:api_url) }
- it { is_expected.to validate_presence_of(:project_url) }
- it { is_expected.to validate_presence_of(:issues_url) }
- it { is_expected.to validate_presence_of(:new_issue_url) }
- it_behaves_like 'issue tracker service URL attribute', :api_url
- it_behaves_like 'issue tracker service URL attribute', :project_url
- it_behaves_like 'issue tracker service URL attribute', :issues_url
- it_behaves_like 'issue tracker service URL attribute', :new_issue_url
+ it { is_expected.to validate_presence_of(:url) }
+ it { is_expected.to validate_presence_of(:project_key) }
+ it_behaves_like 'issue tracker service URL attribute', :url
end
context 'when service is inactive' do
before { subject.active = false }
- it { is_expected.not_to validate_presence_of(:api_url) }
- it { is_expected.not_to validate_presence_of(:project_url) }
- it { is_expected.not_to validate_presence_of(:issues_url) }
- it { is_expected.not_to validate_presence_of(:new_issue_url) }
+ it { is_expected.not_to validate_presence_of(:url) }
end
end
@@ -50,23 +42,25 @@ describe JiraService, models: true do
project_id: project.id,
project: project,
service_hook: true,
- project_url: 'http://jira.example.com',
+ url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password'
)
- @jira_service.save # will build API URL, as api_url was not specified above
- @sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
- # https://github.com/bblimke/webmock#request-with-basic-authentication
- @api_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
+
+ @jira_service.save
+
+ project_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123'
+ @transitions_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/transitions'
@comment_url = 'http://gitlab_jira_username:gitlab_jira_password@jira.example.com/rest/api/2/issue/JIRA-123/comment'
- WebMock.stub_request(:post, @api_url)
+ WebMock.stub_request(:get, project_url)
+ WebMock.stub_request(:post, @transitions_url)
WebMock.stub_request(:post, @comment_url)
end
it "calls JIRA API" do
- @jira_service.execute(merge_request,
- ExternalIssue.new("JIRA-123", project))
+ @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
expect(WebMock).to have_requested(:post, @comment_url).with(
body: /Issue solved with/
).once
@@ -74,9 +68,9 @@ describe JiraService, models: true do
it "calls the api with jira_issue_transition_id" do
@jira_service.jira_issue_transition_id = 'this-is-a-custom-id'
- @jira_service.execute(merge_request,
- ExternalIssue.new("JIRA-123", project))
- expect(WebMock).to have_requested(:post, @api_url).with(
+ @jira_service.execute(merge_request, ExternalIssue.new("JIRA-123", project))
+
+ expect(WebMock).to have_requested(:post, @transitions_url).with(
body: /this-is-a-custom-id/
).once
end
@@ -90,7 +84,7 @@ describe JiraService, models: true do
@jira_service = JiraService.create!(
project: create(:project),
properties: {
- api_url: 'http://jira.example.com/rest/api/2',
+ url: 'http://jira.example.com/rest/api/2',
username: 'mic',
password: "password"
}
@@ -98,7 +92,7 @@ describe JiraService, models: true do
end
it "reset password if url changed" do
- @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
@@ -110,16 +104,16 @@ describe JiraService, models: true do
end
it "does not reset password if new url is set together with password, even if it's the same password" do
- @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
expect(@jira_service.password).to eq("password")
- expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
+ expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2")
end
it "resets password if url changed, even if setter called multiple times" do
- @jira_service.api_url = 'http://jira1.example.com/rest/api/2'
- @jira_service.api_url = 'http://jira1.example.com/rest/api/2'
+ @jira_service.url = 'http://jira1.example.com/rest/api/2'
+ @jira_service.url = 'http://jira1.example.com/rest/api/2'
@jira_service.save
expect(@jira_service.password).to be_nil
end
@@ -130,18 +124,18 @@ describe JiraService, models: true do
@jira_service = JiraService.create(
project: create(:project),
properties: {
- api_url: 'http://jira.example.com/rest/api/2',
+ url: 'http://jira.example.com/rest/api/2',
username: 'mic'
}
)
end
it "saves password if new url is set together with password" do
- @jira_service.api_url = 'http://jira_edited.example.com/rest/api/2'
+ @jira_service.url = 'http://jira_edited.example.com/rest/api/2'
@jira_service.password = 'password'
@jira_service.save
expect(@jira_service.password).to eq("password")
- expect(@jira_service.api_url).to eq("http://jira_edited.example.com/rest/api/2")
+ expect(@jira_service.url).to eq("http://jira_edited.example.com/rest/api/2")
end
end
end
@@ -152,9 +146,7 @@ describe JiraService, models: true do
subject.active = true
end
- it { is_expected.to validate_presence_of :project_url }
- it { is_expected.to validate_presence_of :issues_url }
- it { is_expected.to validate_presence_of :new_issue_url }
+ it { is_expected.to validate_presence_of :url }
end
end
@@ -201,9 +193,7 @@ describe JiraService, models: true do
settings = {
"jira" => {
"title" => "Jira",
- "project_url" => "http://jira.sample/projects/project_a",
- "issues_url" => "http://jira.sample/issues/:id",
- "new_issue_url" => "http://jira.sample/projects/project_a/issues/new"
+ "url" => "http://jira.sample/projects/project_a"
}
}
allow(Gitlab.config).to receive(:issues_tracker).and_return(settings)
@@ -215,9 +205,8 @@ describe JiraService, models: true do
end
it 'is prepopulated with the settings' do
- expect(@service.properties["project_url"]).to eq('http://jira.sample/projects/project_a')
- expect(@service.properties["issues_url"]).to eq("http://jira.sample/issues/:id")
- expect(@service.properties["new_issue_url"]).to eq("http://jira.sample/projects/project_a/issues/new")
+ expect(@service.properties["title"]).to eq('Jira')
+ expect(@service.properties["url"]).to eq('http://jira.sample/projects/project_a')
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f4dda1ee558..aef277357cf 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -67,11 +67,11 @@ describe Project, models: true do
it { is_expected.to have_many(:notification_settings).dependent(:destroy) }
it { is_expected.to have_many(:forks).through(:forked_project_links) }
- context 'after create' do
- it "creates project feature" do
+ context 'after initialized' do
+ it "has a project_feature" do
project = FactoryGirl.build(:project)
- expect { project.save }.to change{ project.project_feature.present? }.from(false).to(true)
+ expect(project.project_feature.present?).to be_present
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 187a1bf2d79..04b7d19d414 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -68,8 +68,8 @@ describe Repository, models: true do
double_first = double(committed_date: Time.now)
double_last = double(committed_date: Time.now - 1.second)
- allow(tag_a).to receive(:target).and_return(double_first)
- allow(tag_b).to receive(:target).and_return(double_last)
+ allow(tag_a).to receive(:dereferenced_target).and_return(double_first)
+ allow(tag_b).to receive(:dereferenced_target).and_return(double_last)
allow(repository).to receive(:tags).and_return([tag_a, tag_b])
end
@@ -83,8 +83,8 @@ describe Repository, models: true do
double_first = double(committed_date: Time.now - 1.second)
double_last = double(committed_date: Time.now)
- allow(tag_a).to receive(:target).and_return(double_last)
- allow(tag_b).to receive(:target).and_return(double_first)
+ allow(tag_a).to receive(:dereferenced_target).and_return(double_last)
+ allow(tag_b).to receive(:dereferenced_target).and_return(double_first)
allow(repository).to receive(:tags).and_return([tag_a, tag_b])
end
@@ -632,9 +632,9 @@ describe Repository, models: true do
context "when the branch wasn't empty" do
it 'updates the head' do
- expect(repository.find_branch('feature').target.id).to eq(old_rev)
+ expect(repository.find_branch('feature').dereferenced_target.id).to eq(old_rev)
repository.update_branch_with_hooks(user, 'feature') { new_rev }
- expect(repository.find_branch('feature').target.id).to eq(new_rev)
+ expect(repository.find_branch('feature').dereferenced_target.id).to eq(new_rev)
end
end
end
@@ -659,7 +659,7 @@ describe Repository, models: true do
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
+ old_rev = repository.find_branch(branch).dereferenced_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)
@@ -1472,4 +1472,14 @@ describe Repository, models: true do
end.to raise_error(Repository::CommitError)
end
end
+
+ describe '#remove_storage_from_path' do
+ let(:storage_path) { project.repository_storage_path }
+ let(:project_path) { project.path_with_namespace }
+ let(:full_path) { File.join(storage_path, project_path) }
+
+ it { expect(Repository.remove_storage_from_path(full_path)).to eq(project_path) }
+ it { expect(Repository.remove_storage_from_path(project_path)).to eq(project_path) }
+ it { expect(Repository.remove_storage_from_path(storage_path)).to eq('') }
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 10c39b90212..d1ed774a914 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -599,6 +599,80 @@ describe User, models: true do
end
end
+ describe '.search_with_secondary_emails' do
+ def search_with_secondary_emails(query)
+ described_class.search_with_secondary_emails(query)
+ end
+
+ let!(:user) { create(:user) }
+ let!(:email) { create(:email) }
+
+ it 'returns users with a matching name' do
+ expect(search_with_secondary_emails(user.name)).to eq([user])
+ end
+
+ it 'returns users with a partially matching name' do
+ expect(search_with_secondary_emails(user.name[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching name regardless of the casing' do
+ expect(search_with_secondary_emails(user.name.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching email' do
+ expect(search_with_secondary_emails(user.email)).to eq([user])
+ end
+
+ it 'returns users with a partially matching email' do
+ expect(search_with_secondary_emails(user.email[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching email regardless of the casing' do
+ expect(search_with_secondary_emails(user.email.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching username' do
+ expect(search_with_secondary_emails(user.username)).to eq([user])
+ end
+
+ it 'returns users with a partially matching username' do
+ expect(search_with_secondary_emails(user.username[0..2])).to eq([user])
+ end
+
+ it 'returns users with a matching username regardless of the casing' do
+ expect(search_with_secondary_emails(user.username.upcase)).to eq([user])
+ end
+
+ it 'returns users with a matching whole secondary email' do
+ expect(search_with_secondary_emails(email.email)).to eq([email.user])
+ end
+
+ it 'returns users with a matching part of secondary email' do
+ expect(search_with_secondary_emails(email.email[1..4])).to eq([email.user])
+ end
+
+ it 'return users with a matching part of secondary email regardless of case' do
+ expect(search_with_secondary_emails(email.email[1..4].upcase)).to eq([email.user])
+ expect(search_with_secondary_emails(email.email[1..4].downcase)).to eq([email.user])
+ expect(search_with_secondary_emails(email.email[1..4].capitalize)).to eq([email.user])
+ end
+
+ it 'returns multiple users with matching secondary emails' do
+ email1 = create(:email, email: '1_testemail@example.com')
+ email2 = create(:email, email: '2_testemail@example.com')
+ email3 = create(:email, email: 'other@email.com')
+ email3.user.update_attributes!(email: 'another@mail.com')
+
+ expect(
+ search_with_secondary_emails('testemail@example.com').map(&:id)
+ ).to include(email1.user.id, email2.user.id)
+
+ expect(
+ search_with_secondary_emails('testemail@example.com').map(&:id)
+ ).not_to include(email3.user.id)
+ end
+ end
+
describe 'by_username_or_id' do
let(:user1) { create(:user, username: 'foo') }
diff --git a/spec/policies/issues_policy_spec.rb b/spec/policies/issues_policy_spec.rb
new file mode 100644
index 00000000000..2b7b6cad654
--- /dev/null
+++ b/spec/policies/issues_policy_spec.rb
@@ -0,0 +1,193 @@
+require 'spec_helper'
+
+describe IssuePolicy, models: true do
+ let(:guest) { create(:user) }
+ let(:author) { create(:user) }
+ let(:assignee) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:reporter_from_group_link) { create(:user) }
+
+ def permissions(user, issue)
+ IssuePolicy.abilities(user, issue).to_set
+ end
+
+ context 'a private project' do
+ let(:non_member) { create(:user) }
+ let(:project) { create(:empty_project, :private) }
+ let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+ let(:issue_no_assignee) { create(:issue, project: project) }
+
+ before do
+ project.team << [guest, :guest]
+ project.team << [author, :guest]
+ project.team << [assignee, :guest]
+ project.team << [reporter, :reporter]
+
+ group.add_reporter(reporter_from_group_link)
+
+ create(:project_group_link, group: group, project: project)
+ end
+
+ it 'does not allow non-members to read issues' do
+ expect(permissions(non_member, issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(non_member, issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows guests to read issues' do
+ expect(permissions(guest, issue)).to include(:read_issue)
+ expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+ expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin issues' do
+ expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their issues' do
+ expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their issues' do
+ expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ context 'with confidential issues' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+ let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
+
+ it 'does not allow non-members to read confidential issues' do
+ expect(permissions(non_member, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(non_member, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'does not allow guests to read confidential issues' do
+ expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin confidential issues' do
+ expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin confidential issues' do
+ expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their confidential issues' do
+ expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their confidential issues' do
+ expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+ end
+ end
+
+ context 'a public project' do
+ let(:project) { create(:empty_project, :public) }
+ let(:issue) { create(:issue, project: project, assignee: assignee, author: author) }
+ let(:issue_no_assignee) { create(:issue, project: project) }
+
+ before do
+ project.team << [guest, :guest]
+ project.team << [reporter, :reporter]
+
+ group.add_reporter(reporter_from_group_link)
+
+ create(:project_group_link, group: group, project: project)
+ end
+
+ it 'allows guests to read issues' do
+ expect(permissions(guest, issue)).to include(:read_issue)
+ expect(permissions(guest, issue)).not_to include(:update_issue, :admin_issue)
+
+ expect(permissions(guest, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(guest, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin issues' do
+ expect(permissions(reporter, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters from group links to read, update, and admin issues' do
+ expect(permissions(reporter_from_group_link, issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their issues' do
+ expect(permissions(author, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(author, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their issues' do
+ expect(permissions(assignee, issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, issue_no_assignee)).to include(:read_issue)
+ expect(permissions(assignee, issue_no_assignee)).not_to include(:update_issue, :admin_issue)
+ end
+
+ context 'with confidential issues' do
+ let(:confidential_issue) { create(:issue, :confidential, project: project, assignee: assignee, author: author) }
+ let(:confidential_issue_no_assignee) { create(:issue, :confidential, project: project) }
+
+ it 'does not allow guests to read confidential issues' do
+ expect(permissions(guest, confidential_issue)).not_to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(guest, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporters to read, update, and admin confidential issues' do
+ expect(permissions(reporter, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows reporter from group links to read, update, and admin confidential issues' do
+ expect(permissions(reporter_from_group_link, confidential_issue)).to include(:read_issue, :update_issue, :admin_issue)
+ expect(permissions(reporter_from_group_link, confidential_issue_no_assignee)).to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue authors to read and update their confidential issues' do
+ expect(permissions(author, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(author, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(author, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+
+ it 'allows issue assignees to read and update their confidential issues' do
+ expect(permissions(assignee, confidential_issue)).to include(:read_issue, :update_issue)
+ expect(permissions(assignee, confidential_issue)).not_to include(:admin_issue)
+
+ expect(permissions(assignee, confidential_issue_no_assignee)).not_to include(:read_issue, :update_issue, :admin_issue)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index 0f41f8dc7f1..01bb9e955e0 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -265,29 +265,6 @@ describe API::Helpers, api: true do
end
end
- describe '.to_boolean' do
- it 'converts a valid string to a boolean' do
- expect(to_boolean('true')).to be_truthy
- expect(to_boolean('YeS')).to be_truthy
- expect(to_boolean('t')).to be_truthy
- expect(to_boolean('1')).to be_truthy
- expect(to_boolean('ON')).to be_truthy
- expect(to_boolean('FaLse')).to be_falsy
- expect(to_boolean('F')).to be_falsy
- expect(to_boolean('NO')).to be_falsy
- expect(to_boolean('n')).to be_falsy
- expect(to_boolean('0')).to be_falsy
- expect(to_boolean('oFF')).to be_falsy
- end
-
- it 'converts an invalid string to nil' do
- expect(to_boolean('fals')).to be_nil
- expect(to_boolean('yeah')).to be_nil
- expect(to_boolean('')).to be_nil
- expect(to_boolean(nil)).to be_nil
- end
- end
-
describe '.handle_api_exception' do
before do
allow_any_instance_of(self.class).to receive(:sentry_enabled?).and_return(true)
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index 905f762d578..1711096f4bd 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -95,18 +95,6 @@ describe API::API, api: true do
expect(json_response['developers_can_push']).to eq(true)
expect(json_response['developers_can_merge']).to eq(true)
end
-
- it 'protects a single branch and developers cannot push and merge' do
- put api("/projects/#{project.id}/repository/branches/#{branch_name}/protect", user),
- developers_can_push: 'tru', developers_can_merge: 'tr'
-
- expect(response).to have_http_status(200)
- expect(json_response['name']).to eq(branch_name)
- expect(json_response['commit']['id']).to eq(branch_sha)
- expect(json_response['protected']).to eq(true)
- expect(json_response['developers_can_push']).to eq(false)
- expect(json_response['developers_can_merge']).to eq(false)
- end
end
context 'for an existing protected branch' do
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index 95c7bbf99c9..fc72a44d663 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -277,6 +277,7 @@ describe API::API, api: true do
context 'with regular branch' do
before do
+ pipeline.reload
pipeline.update(ref: 'master',
sha: project.commit('master').sha)
@@ -288,6 +289,7 @@ describe API::API, api: true do
context 'with branch name containing slash' do
before do
+ pipeline.reload
pipeline.update(ref: 'improve/awesome',
sha: project.commit('improve/awesome').sha)
end
diff --git a/spec/requests/api/deploy_keys_spec.rb b/spec/requests/api/deploy_keys_spec.rb
index 7d8cc45327c..65897edba7f 100644
--- a/spec/requests/api/deploy_keys_spec.rb
+++ b/spec/requests/api/deploy_keys_spec.rb
@@ -6,6 +6,7 @@ describe API::API, api: true do
let(:user) { create(:user) }
let(:admin) { create(:admin) }
let(:project) { create(:project, creator_id: user.id) }
+ let(:project2) { create(:project, creator_id: user.id) }
let(:deploy_key) { create(:deploy_key, public: true) }
let!(:deploy_keys_project) do
@@ -96,6 +97,22 @@ describe API::API, api: true do
post api("/projects/#{project.id}/deploy_keys", admin), key_attrs
end.to change{ project.deploy_keys.count }.by(1)
end
+
+ it 'returns an existing ssh key when attempting to add a duplicate' do
+ expect do
+ post api("/projects/#{project.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.not_to change { project.deploy_keys.count }
+
+ expect(response).to have_http_status(201)
+ end
+
+ it 'joins an existing ssh key to a new project' do
+ expect do
+ post api("/projects/#{project2.id}/deploy_keys", admin), { key: deploy_key.key, title: deploy_key.title }
+ end.to change { project2.deploy_keys.count }.by(1)
+
+ expect(response).to have_http_status(201)
+ end
end
describe 'DELETE /projects/:id/deploy_keys/:key_id' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index d48752473f3..ae8639d78d5 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -958,6 +958,29 @@ describe API::API, api: true do
expect(joined_event['author']['name']).to eq(user.name)
end
end
+
+ context 'when there are multiple events from different projects' do
+ let(:second_note) { create(:note_on_issue, project: create(:empty_project)) }
+ let(:third_note) { create(:note_on_issue, project: project) }
+
+ before do
+ second_note.project.add_user(user, :developer)
+
+ [second_note, third_note].each do |note|
+ EventCreateService.new.leave_note(note, user)
+ end
+ end
+
+ it 'returns events in the correct order (from newest to oldest)' do
+ get api("/users/#{user.id}/events", user)
+
+ comment_events = json_response.select { |e| e['action_name'] == 'commented on' }
+
+ expect(comment_events[0]['target_id']).to eq(third_note.id)
+ expect(comment_events[1]['target_id']).to eq(second_note.id)
+ expect(comment_events[2]['target_id']).to eq(note.id)
+ end
+ end
end
it 'returns a 404 error if not found' do
diff --git a/spec/services/ci/register_build_service_spec.rb b/spec/services/ci/register_build_service_spec.rb
index 1e21a32a062..a3fc23ba177 100644
--- a/spec/services/ci/register_build_service_spec.rb
+++ b/spec/services/ci/register_build_service_spec.rb
@@ -101,11 +101,11 @@ module Ci
it 'equalises number of running builds' do
# after finishing the first build for project 1, get a second build from the same project
expect(service.execute(shared_runner)).to eq(build1_project1)
- build1_project1.success
+ build1_project1.reload.success
expect(service.execute(shared_runner)).to eq(build2_project1)
expect(service.execute(shared_runner)).to eq(build1_project2)
- build1_project2.success
+ build1_project2.reload.success
expect(service.execute(shared_runner)).to eq(build2_project2)
expect(service.execute(shared_runner)).to eq(build1_project3)
expect(service.execute(shared_runner)).to eq(build3_project1)
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index ad5170afc21..45bc44ba172 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -451,11 +451,7 @@ describe GitPushService, services: true do
# project.create_jira_service doesn't seem to invalidate the cache here
project.has_external_issue_tracker = true
jira_service_settings
-
- WebMock.stub_request(:post, jira_api_transition_url)
- WebMock.stub_request(:post, jira_api_comment_url)
- WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
- WebMock.stub_request(:get, jira_api_test_url)
+ stub_jira_urls("JIRA-1")
allow(closing_commit).to receive_messages({
issue_closing_regex: Regexp.new(Gitlab.config.gitlab.issue_closing_pattern),
@@ -475,9 +471,9 @@ describe GitPushService, services: true do
let(:message) { "this is some work.\n\nrelated to JIRA-1" }
it "initiates one api call to jira server to mention the issue" do
- execute_service(project, user, @oldrev, @newrev, @ref )
+ execute_service(project, user, @oldrev, @newrev, @ref)
- expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
+ expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: /mentioned this issue in/
).once
end
@@ -485,22 +481,19 @@ describe GitPushService, services: true do
context "closing an issue" do
let(:message) { "this is some work.\n\ncloses JIRA-1" }
- let(:transition_body) { { transition: { id: '2' } }.to_json }
let(:comment_body) { { body: "Issue solved with [#{closing_commit.id}|http://localhost/#{project.path_with_namespace}/commit/#{closing_commit.id}]." }.to_json }
context "using right markdown" do
it "initiates one api call to jira server to close the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
- expect(WebMock).to have_requested(:post, jira_api_transition_url).with(
- body: transition_body
- ).once
+ expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once
end
it "initiates one api call to jira server to comment on the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
- expect(WebMock).to have_requested(:post, jira_api_comment_url).with(
+ expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
).once
end
@@ -512,15 +505,13 @@ describe GitPushService, services: true do
it "does not initiates one api call to jira server to close the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
- expect(WebMock).not_to have_requested(:post, jira_api_transition_url).with(
- body: transition_body
- )
+ expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
end
it "does not initiates one api call to jira server to comment on the issue" do
execute_service(project, commit_author, @oldrev, @newrev, @ref )
- expect(WebMock).not_to have_requested(:post, jira_api_comment_url).with(
+ expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
body: comment_body
).once
end
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index a4fcd44882d..0879e3ab4c8 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -37,65 +37,138 @@ describe GitTagPushService, services: true do
end
describe "Git Tag Push Data" do
- before do
- service.execute
- @push_data = service.push_data
- @tag_name = Gitlab::Git.ref_name(ref)
- @tag = project.repository.find_tag(@tag_name)
- @commit = project.commit(@tag.target)
- end
-
subject { @push_data }
+ let(:tag) { project.repository.find_tag(tag_name) }
+ let(:commit) { tag.dereferenced_target }
- it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: ref) }
- it { is_expected.to include(before: oldrev) }
- it { is_expected.to include(after: newrev) }
- it { is_expected.to include(message: @tag.message) }
- it { is_expected.to include(user_id: user.id) }
- it { is_expected.to include(user_name: user.name) }
- it { is_expected.to include(project_id: project.id) }
-
- context "with repository data" do
- subject { @push_data[:repository] }
-
- it { is_expected.to include(name: project.name) }
- it { is_expected.to include(url: project.url_to_repo) }
- it { is_expected.to include(description: project.description) }
- it { is_expected.to include(homepage: project.web_url) }
- end
+ context 'annotated tag' do
+ let(:tag_name) { Gitlab::Git.ref_name(ref) }
- context "with commits" do
- subject { @push_data[:commits] }
+ before do
+ service.execute
+ @push_data = service.push_data
+ end
- it { is_expected.to be_an(Array) }
- it 'has 1 element' do
- expect(subject.size).to eq(1)
+ it { is_expected.to include(object_kind: 'tag_push') }
+ it { is_expected.to include(ref: ref) }
+ it { is_expected.to include(before: oldrev) }
+ it { is_expected.to include(after: newrev) }
+ it { is_expected.to include(message: tag.message) }
+ it { is_expected.to include(user_id: user.id) }
+ it { is_expected.to include(user_name: user.name) }
+ it { is_expected.to include(project_id: project.id) }
+
+ context "with repository data" do
+ subject { @push_data[:repository] }
+
+ it { is_expected.to include(name: project.name) }
+ it { is_expected.to include(url: project.url_to_repo) }
+ it { is_expected.to include(description: project.description) }
+ it { is_expected.to include(homepage: project.web_url) }
end
- context "the commit" do
- subject { @push_data[:commits].first }
-
- it { is_expected.to include(id: @commit.id) }
- it { is_expected.to include(message: @commit.safe_message) }
- it { is_expected.to include(timestamp: @commit.date.xmlschema) }
- it do
- is_expected.to include(
- url: [
- Gitlab.config.gitlab.url,
- project.namespace.to_param,
- project.to_param,
- 'commit',
- @commit.id
- ].join('/')
- )
+ context "with commits" do
+ subject { @push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
+
+ context "the commit" do
+ subject { @push_data[:commits].first }
+
+ it { is_expected.to include(id: commit.id) }
+ it { is_expected.to include(message: commit.safe_message) }
+ it { is_expected.to include(timestamp: commit.date.xmlschema) }
+ it do
+ is_expected.to include(
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ commit.id
+ ].join('/')
+ )
+ end
+
+ context "with a author" do
+ subject { @push_data[:commits].first[:author] }
+
+ it { is_expected.to include(name: commit.author_name) }
+ it { is_expected.to include(email: commit.author_email) }
+ end
end
+ end
+ end
- context "with a author" do
- subject { @push_data[:commits].first[:author] }
+ context 'lightweight tag' do
+ let(:tag_name) { 'light-tag' }
+ let(:newrev) { '5937ac0a7beb003549fc5fd26fc247adbce4a52e' }
+ let(:ref) { "refs/tags/light-tag" }
+
+ before do
+ # Create the lightweight tag
+ project.repository.raw_repository.rugged.tags.create(tag_name, newrev)
+
+ # Clear tag list cache
+ project.repository.expire_tags_cache
+
+ service.execute
+ @push_data = service.push_data
+ end
+
+ it { is_expected.to include(object_kind: 'tag_push') }
+ it { is_expected.to include(ref: ref) }
+ it { is_expected.to include(before: oldrev) }
+ it { is_expected.to include(after: newrev) }
+ it { is_expected.to include(message: tag.message) }
+ it { is_expected.to include(user_id: user.id) }
+ it { is_expected.to include(user_name: user.name) }
+ it { is_expected.to include(project_id: project.id) }
+
+ context "with repository data" do
+ subject { @push_data[:repository] }
+
+ it { is_expected.to include(name: project.name) }
+ it { is_expected.to include(url: project.url_to_repo) }
+ it { is_expected.to include(description: project.description) }
+ it { is_expected.to include(homepage: project.web_url) }
+ end
+
+ context "with commits" do
+ subject { @push_data[:commits] }
+
+ it { is_expected.to be_an(Array) }
+ it 'has 1 element' do
+ expect(subject.size).to eq(1)
+ end
- it { is_expected.to include(name: @commit.author_name) }
- it { is_expected.to include(email: @commit.author_email) }
+ context "the commit" do
+ subject { @push_data[:commits].first }
+
+ it { is_expected.to include(id: commit.id) }
+ it { is_expected.to include(message: commit.safe_message) }
+ it { is_expected.to include(timestamp: commit.date.xmlschema) }
+ it do
+ is_expected.to include(
+ url: [
+ Gitlab.config.gitlab.url,
+ project.namespace.to_param,
+ project.to_param,
+ 'commit',
+ commit.id
+ ].join('/')
+ )
+ end
+
+ context "with a author" do
+ subject { @push_data[:commits].first[:author] }
+
+ it { is_expected.to include(name: commit.author_name) }
+ it { is_expected.to include(email: commit.author_email) }
+ end
end
end
end
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
index cbfc63de811..7a9b34f9f96 100644
--- a/spec/services/labels/find_or_create_service_spec.rb
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -2,7 +2,6 @@ require 'spec_helper'
describe Labels::FindOrCreateService, services: true do
describe '#execute' do
- let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, namespace: group) }
@@ -14,37 +13,49 @@ describe Labels::FindOrCreateService, services: true do
}
end
- subject(:service) { described_class.new(user, project, params) }
-
- before do
- project.team << [user, :developer]
- end
+ context 'when acting on behalf of a specific user' do
+ let(:user) { create(:user) }
+ subject(:service) { described_class.new(user, project, params) }
+ before do
+ project.team << [user, :developer]
+ end
- context 'when label does not exist at group level' do
- it 'creates a new label at project level' do
- expect { service.execute }.to change(project.labels, :count).by(1)
+ context 'when label does not exist at group level' do
+ it 'creates a new label at project level' do
+ expect { service.execute }.to change(project.labels, :count).by(1)
+ end
end
- end
- context 'when label exists at group level' do
- it 'returns the group label' do
- group_label = create(:group_label, group: group, title: 'Security')
+ context 'when label exists at group level' do
+ it 'returns the group label' do
+ group_label = create(:group_label, group: group, title: 'Security')
- expect(service.execute).to eq group_label
+ expect(service.execute).to eq group_label
+ end
end
- end
- context 'when label does not exist at group level' do
- it 'creates a new label at project leve' do
- expect { service.execute }.to change(project.labels, :count).by(1)
+ context 'when label does not exist at group level' do
+ it 'creates a new label at project leve' do
+ expect { service.execute }.to change(project.labels, :count).by(1)
+ end
+ end
+
+ context 'when label exists at project level' do
+ it 'returns the project label' do
+ project_label = create(:label, project: project, title: 'Security')
+
+ expect(service.execute).to eq project_label
+ end
end
end
- context 'when label exists at project level' do
+ context 'when authorization is not required' do
+ subject(:service) { described_class.new(nil, project, params) }
+
it 'returns the project label' do
project_label = create(:label, project: project, title: 'Security')
- expect(service.execute).to eq project_label
+ expect(service.execute(skip_authorization: true)).to eq project_label
end
end
end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index 03e296259f9..7b090343a3e 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -5,36 +5,37 @@ describe Members::ApproveAccessRequestService, services: true do
let(:access_requester) { create(:user) }
let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
+ let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
it 'raises ActiveRecord::RecordNotFound' do
- expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
- expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service approving an access request' do
it 'succeeds' do
- expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1)
+ expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1)
end
it 'returns a <Source>Member' do
- member = described_class.new(source, user, params).execute
+ member = described_class.new(source, user, params).execute(opts)
expect(member).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_nil
end
context 'with a custom access level' do
- let(:params) { { user_id: access_requester.id, access_level: Gitlab::Access::MASTER } }
+ let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) }
it 'returns a ProjectMember with the custom access level' do
- member = described_class.new(source, user, params).execute
+ member = described_class.new(source, user, params2).execute(opts)
expect(member.access_level).to eq Gitlab::Access::MASTER
end
@@ -60,6 +61,56 @@ describe Members::ApproveAccessRequestService, services: true do
end
let(:params) { { user_id: access_requester.id } }
+ context 'when current user is nil' do
+ let(:user) { nil }
+
+ context 'and :force option is not given' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+
+ context 'and :force option is false' do
+ let(:opts) { { force: false } }
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+
+ context 'and :force option is true' do
+ let(:opts) { { force: true } }
+
+ it_behaves_like 'a service approving an access request' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service approving an access request' do
+ let(:source) { group }
+ end
+ end
+
+ context 'and :force param is true' do
+ let(:params) { { user_id: access_requester.id, force: true } }
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+ end
+
context 'when current user cannot approve access request to the project' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
new file mode 100644
index 00000000000..0670ac2faa2
--- /dev/null
+++ b/spec/services/members/create_service_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Members::CreateService, services: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:project_user) { create(:user) }
+
+ before { project.team << [user, :master] }
+
+ it 'adds user to members' do
+ params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST }
+ result = described_class.new(project, user, params).execute
+
+ expect(result).to be_truthy
+ expect(project.users).to include project_user
+ end
+
+ it 'adds no user to members' do
+ params = { user_ids: '', access_level: Gitlab::Access::GUEST }
+ result = described_class.new(project, user, params).execute
+
+ expect(result).to be_falsey
+ expect(project.users).not_to include project_user
+ end
+end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 3a3f07ddcb9..3f5df049ea2 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -25,6 +25,8 @@ describe MergeRequests::BuildService, services: true do
before do
allow(CompareService).to receive_message_chain(:new, :execute).and_return(compare)
+ allow(project).to receive(:commit).and_return(commit_1)
+ allow(project).to receive(:commit).and_return(commit_2)
end
describe 'execute' do
@@ -193,5 +195,52 @@ describe MergeRequests::BuildService, services: true do
end
end
end
+
+ context 'source branch does not exist' do
+ before do
+ allow(project).to receive(:commit).with(source_branch).and_return(nil)
+ allow(project).to receive(:commit).with(target_branch).and_return(commit_1)
+ end
+
+ it 'forbids the merge request from being created' do
+ expect(merge_request.can_be_created).to eq(false)
+ end
+
+ it 'adds an error message to the merge request' do
+ expect(merge_request.errors).to contain_exactly('Source branch "feature-branch" does not exist')
+ end
+ end
+
+ context 'target branch does not exist' do
+ before do
+ allow(project).to receive(:commit).with(source_branch).and_return(commit_1)
+ allow(project).to receive(:commit).with(target_branch).and_return(nil)
+ end
+
+ it 'forbids the merge request from being created' do
+ expect(merge_request.can_be_created).to eq(false)
+ end
+
+ it 'adds an error message to the merge request' do
+ expect(merge_request.errors).to contain_exactly('Target branch "master" does not exist')
+ end
+ end
+
+ context 'both source and target branches do not exist' do
+ before do
+ allow(project).to receive(:commit).and_return(nil)
+ end
+
+ it 'forbids the merge request from being created' do
+ expect(merge_request.can_be_created).to eq(false)
+ end
+
+ it 'adds both error messages to the merge request' do
+ expect(merge_request.errors).to contain_exactly(
+ 'Source branch "feature-branch" does not exist',
+ 'Target branch "master" does not exist'
+ )
+ end
+ end
end
end
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index b80cfd8f450..1f90efdbd6a 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -147,6 +147,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
expect(MergeWorker).not_to receive(:perform_async)
build.success
+ test.reload
test.drop
end
@@ -154,6 +155,7 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
expect(MergeWorker).to receive(:perform_async)
build.success
+ test.reload
test.success
end
end
diff --git a/spec/services/milestones/close_service_spec.rb b/spec/services/milestones/close_service_spec.rb
index 5d400299be0..92b84308f73 100644
--- a/spec/services/milestones/close_service_spec.rb
+++ b/spec/services/milestones/close_service_spec.rb
@@ -18,7 +18,7 @@ describe Milestones::CloseService, services: true do
it { expect(milestone).to be_closed }
describe :event do
- let(:event) { Event.first }
+ let(:event) { Event.recent.first }
it { expect(event.milestone).to be_truthy }
it { expect(event.target).to eq(milestone) }
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 3ea1273abc3..876bfaf085c 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -69,7 +69,7 @@ describe Projects::CreateService, services: true do
context 'wiki_enabled false does not create wiki repository directory' do
before do
- @opts.merge!( { project_feature_attributes: { wiki_access_level: ProjectFeature::DISABLED } })
+ @opts.merge!(wiki_enabled: false)
@project = create_project(@user, @opts)
@path = ProjectWiki.new(@project, @user).send(:path_to_repo)
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index ed1384798ab..ab6e8f537ba 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -110,7 +110,7 @@ describe Projects::ImportService, services: true do
end
it 'expires existence cache after error' do
- allow_any_instance_of(Project).to receive(:repository_exists?).and_return(true)
+ allow_any_instance_of(Project).to receive(:repository_exists?).and_return(false, true)
expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index b4ba28dfe8e..5bb107fdd85 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -531,53 +531,47 @@ describe SystemNoteService, services: true do
include JiraServiceHelper
describe 'JIRA integration' do
- let(:project) { create(:jira_project) }
- let(:author) { create(:user) }
- let(:issue) { create(:issue, project: project) }
- let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
- let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
- let(:jira_tracker) { project.jira_service }
- let(:commit) { project.repository.commits('master').find { |commit| commit.id == '5937ac0a7beb003549fc5fd26fc247adbce4a52e' } }
+ let(:project) { create(:jira_project) }
+ let(:author) { create(:user) }
+ let(:issue) { create(:issue, project: project) }
+ let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
+ let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
+ let(:jira_tracker) { project.jira_service }
+ let(:commit) { project.commit }
+ let(:comment_url) { jira_api_comment_url(jira_issue.id) }
+ let(:success_message) { "JiraService SUCCESS: Successfully posted to http://jira.example.net." }
+
+ before { stub_jira_urls(jira_issue.id) }
context 'in JIRA issue tracker' do
- before do
- jira_service_settings
- WebMock.stub_request(:post, jira_api_comment_url)
- end
+ before { jira_service_settings }
describe "new reference" do
- before do
- WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
- end
-
subject { described_class.cross_reference(jira_issue, commit, author) }
- it { is_expected.to eq(jira_status_message) }
- end
-
- describe "existing reference" do
- before do
- message = %Q{[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
- WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]}))
- end
-
- subject { described_class.cross_reference(jira_issue, commit, author) }
- it { is_expected.not_to eq(jira_status_message) }
+ it { is_expected.to eq(success_message) }
end
end
context 'issue from an issue' do
context 'in JIRA issue tracker' do
- before do
- jira_service_settings
- WebMock.stub_request(:post, jira_api_comment_url)
- WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
- end
+ before { jira_service_settings }
subject { described_class.cross_reference(jira_issue, issue, author) }
- it { is_expected.to eq(jira_status_message) }
+ it { is_expected.to eq(success_message) }
+ end
+ end
+
+ describe "existing reference" do
+ before do
+ message = "[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\n'#{commit.title}'"
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:comments).and_return([OpenStruct.new(body: message)])
end
+
+ subject { described_class.cross_reference(jira_issue, commit, author) }
+
+ it { is_expected.not_to eq(success_message) }
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index b19f5824236..b2ca856f89f 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -9,7 +9,7 @@ require 'shoulda/matchers'
require 'sidekiq/testing/inline'
require 'rspec/retry'
-if ENV['CI']
+if ENV['CI'] && !ENV['NO_KNAPSACK']
require 'knapsack'
Knapsack::Adapters::RSpecAdapter.bind
end
@@ -50,6 +50,12 @@ RSpec.configure do |config|
example.run
Rails.cache = caching_store
end
+
+ config.around(:each, :redis) do |example|
+ Gitlab::Redis.with(&:flushall)
+ example.run
+ Gitlab::Redis.with(&:flushall)
+ end
end
FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/banzai/reference_filter_shared_examples.rb b/spec/support/banzai/reference_filter_shared_examples.rb
new file mode 100644
index 00000000000..eb5da662ab5
--- /dev/null
+++ b/spec/support/banzai/reference_filter_shared_examples.rb
@@ -0,0 +1,13 @@
+# Specs for reference links containing HTML.
+#
+# Requires a reference:
+# let(:reference) { '#42' }
+shared_examples 'a reference containing an element node' do
+ let(:inner_html) { 'element <code>node</code> inside' }
+ let(:reference_with_element) { %{<a href="#{reference}">#{inner_html}</a>} }
+
+ it 'does not escape inner html' do
+ doc = reference_filter(reference_with_element)
+ expect(doc.children.first.inner_html).to eq(inner_html)
+ end
+end
diff --git a/spec/support/javascript_fixtures_helpers.rb b/spec/support/javascript_fixtures_helpers.rb
new file mode 100644
index 00000000000..adc3f48b434
--- /dev/null
+++ b/spec/support/javascript_fixtures_helpers.rb
@@ -0,0 +1,45 @@
+require 'fileutils'
+require 'gitlab/popen'
+
+module JavaScriptFixturesHelpers
+ include Gitlab::Popen
+
+ FIXTURE_PATH = 'spec/javascripts/fixtures'
+
+ # Public: Removes all fixture files from given directory
+ #
+ # directory_name - directory of the fixtures (relative to FIXTURE_PATH)
+ #
+ def clean_frontend_fixtures(directory_name)
+ directory_name = File.expand_path(directory_name, FIXTURE_PATH)
+ Dir[File.expand_path('*.html.raw', directory_name)].each do |file_name|
+ FileUtils.rm(file_name)
+ end
+ end
+
+ # Public: Store a response object as fixture file
+ #
+ # response - response object to store
+ # fixture_file_name - file name to store the fixture in (relative to FIXTURE_PATH)
+ #
+ def store_frontend_fixture(response, fixture_file_name)
+ fixture_file_name = File.expand_path(fixture_file_name, FIXTURE_PATH)
+ fixture = response.body
+
+ response_mime_type = Mime::Type.lookup(response.content_type)
+ if response_mime_type.html?
+ doc = Nokogiri::HTML::DocumentFragment.parse(fixture)
+
+ scripts = doc.css('script')
+ scripts.remove
+
+ fixture = doc.to_html
+
+ # replace relative links
+ fixture.gsub!(%r{="/}, '="https://fixture.invalid/')
+ end
+
+ FileUtils.mkdir_p(File.dirname(fixture_file_name))
+ File.write(fixture_file_name, fixture)
+ end
+end
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index f3ea206f387..96e0dad6b55 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -1,20 +1,17 @@
module JiraServiceHelper
+ JIRA_URL = "http://jira.example.net"
+ JIRA_API = JIRA_URL + "/rest/api/2"
+
def jira_service_settings
properties = {
- "title" => "JIRA tracker",
- "project_url" => "http://jira.example/issues/?jql=project=A",
- "issues_url" => "http://jira.example/browse/JIRA-1",
- "new_issue_url" => "http://jira.example/secure/CreateIssue.jspa",
- "api_url" => "http://jira.example/rest/api/2"
+ title: "JIRA tracker",
+ url: JIRA_URL,
+ project_key: "JIRA"
}
jira_tracker.update_attributes(properties: properties, active: true)
end
- def jira_status_message
- "JiraService SUCCESS 200: Successfully posted to #{jira_api_comment_url}."
- end
-
def jira_issue_comments
"{\"startAt\":0,\"maxResults\":11,\"total\":11,
\"comments\":[{\"self\":\"http://0.0.0.0:4567/rest/api/2/issue/10002/comment/10609\",
@@ -52,15 +49,32 @@ module JiraServiceHelper
]}"
end
- def jira_api_comment_url
- 'http://jira.example/rest/api/2/issue/JIRA-1/comment'
+ def jira_project_url
+ JIRA_API + "/project/#{jira_tracker.project_key}"
+ end
+
+ def jira_api_comment_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/comment"
end
- def jira_api_transition_url
- 'http://jira.example/rest/api/2/issue/JIRA-1/transitions'
+ def jira_api_transition_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}/transitions"
end
def jira_api_test_url
- 'http://jira.example/rest/api/2/myself'
+ JIRA_API + "/myself"
+ end
+
+ def jira_issue_url(issue_id)
+ JIRA_API + "/issue/#{issue_id}"
+ end
+
+ def stub_jira_urls(issue_id)
+ WebMock.stub_request(:get, jira_project_url)
+ WebMock.stub_request(:get, jira_api_comment_url(issue_id)).to_return(body: jira_issue_comments)
+ WebMock.stub_request(:get, jira_issue_url(issue_id))
+ WebMock.stub_request(:get, jira_api_test_url)
+ WebMock.stub_request(:post, jira_api_comment_url(issue_id))
+ WebMock.stub_request(:post, jira_api_transition_url(issue_id))
end
end
diff --git a/spec/support/taskable_shared_examples.rb b/spec/support/taskable_shared_examples.rb
index 201614e45a4..ad1c783df4d 100644
--- a/spec/support/taskable_shared_examples.rb
+++ b/spec/support/taskable_shared_examples.rb
@@ -17,6 +17,8 @@ shared_examples 'a Taskable' do
it 'returns the correct task status' do
expect(subject.task_status).to match('2 of')
expect(subject.task_status).to match('5 tasks completed')
+ expect(subject.task_status_short).to match('2/')
+ expect(subject.task_status_short).to match('5 tasks')
end
describe '#tasks?' do
@@ -41,6 +43,8 @@ shared_examples 'a Taskable' do
it 'returns the correct task status' do
expect(subject.task_status).to match('0 of')
expect(subject.task_status).to match('1 task completed')
+ expect(subject.task_status_short).to match('0/')
+ expect(subject.task_status_short).to match('1 task')
end
end
@@ -54,6 +58,8 @@ shared_examples 'a Taskable' do
it 'returns the correct task status' do
expect(subject.task_status).to match('1 of')
expect(subject.task_status).to match('1 task completed')
+ expect(subject.task_status_short).to match('1/')
+ expect(subject.task_status_short).to match('1 task')
end
end
end
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 73bc8326f02..287d83344db 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -79,7 +79,7 @@ describe 'gitlab:app namespace rake task' do
end
end # backup_restore task
- describe 'backup_create' do
+ describe 'backup' do
def tars_glob
Dir.glob(File.join(Gitlab.config.backup.path, '*_gitlab_backup.tar'))
end
@@ -98,6 +98,78 @@ describe 'gitlab:app namespace rake task' do
@backup_tar = tars_glob.first
end
+ def restore_backup
+ orig_stdout = $stdout
+ $stdout = StringIO.new
+ reenable_backup_sub_tasks
+ run_rake_task('gitlab:backup:restore')
+ reenable_backup_sub_tasks
+ $stdout = orig_stdout
+ end
+
+ describe 'backup creation and deletion using annex and custom_hooks' do
+ let(:project) { create(:project) }
+ let(:user_backup_path) { "repositories/#{project.path_with_namespace}" }
+
+ before(:each) do
+ @origin_cd = Dir.pwd
+
+ path = File.join(project.repository.path_to_repo, filename)
+ FileUtils.mkdir_p(path)
+ FileUtils.touch(File.join(path, "dummy.txt"))
+
+ # We need to use the full path instead of the relative one
+ allow(Gitlab.config.gitlab_shell).to receive(:path).and_return(File.expand_path(Gitlab.config.gitlab_shell.path, Rails.root.to_s))
+
+ ENV["SKIP"] = "db"
+ create_backup
+ end
+
+ after(:each) do
+ ENV["SKIP"] = ""
+ FileUtils.rm(@backup_tar)
+ Dir.chdir(@origin_cd)
+ end
+
+ context 'project uses git-annex and successfully creates backup' do
+ let(:filename) { "annex" }
+
+ it 'creates annex.tar and project bundle' do
+ tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}})
+
+ expect(exit_status).to eq(0)
+ expect(tar_contents).to match(user_backup_path)
+ expect(tar_contents).to match("#{user_backup_path}/annex.tar")
+ expect(tar_contents).to match("#{user_backup_path}.bundle")
+ end
+
+ it 'restores files correctly' do
+ restore_backup
+
+ expect(Dir.entries(File.join(project.repository.path, "annex"))).to include("dummy.txt")
+ end
+ end
+
+ context 'project uses custom_hooks and successfully creates backup' do
+ let(:filename) { "custom_hooks" }
+
+ it 'creates custom_hooks.tar and project bundle' do
+ tar_contents, exit_status = Gitlab::Popen.popen(%W{tar -tvf #{@backup_tar}})
+
+ expect(exit_status).to eq(0)
+ expect(tar_contents).to match(user_backup_path)
+ expect(tar_contents).to match("#{user_backup_path}/custom_hooks.tar")
+ expect(tar_contents).to match("#{user_backup_path}.bundle")
+ end
+
+ it 'restores files correctly' do
+ restore_backup
+
+ expect(Dir.entries(File.join(project.repository.path, "custom_hooks"))).to include("dummy.txt")
+ end
+ end
+ end
+
context 'tar creation' do
before do
create_backup
diff --git a/spec/views/projects/commit/_commit_box.html.haml_spec.rb b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
new file mode 100644
index 00000000000..16bf0698c4b
--- /dev/null
+++ b/spec/views/projects/commit/_commit_box.html.haml_spec.rb
@@ -0,0 +1,28 @@
+require 'spec_helper'
+
+describe 'projects/commit/_commit_box.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ let(:project) { create(:project) }
+
+ before do
+ assign(:project, project)
+ assign(:commit, project.commit)
+ end
+
+ it 'shows the commit SHA' do
+ render
+
+ expect(rendered).to have_text("Commit #{Commit.truncate_sha(project.commit.sha)}")
+ end
+
+ it 'shows the last pipeline that ran for the commit' do
+ create(:ci_pipeline, project: project, sha: project.commit.id, status: 'success')
+ create(:ci_pipeline, project: project, sha: project.commit.id, status: 'canceled')
+ third_pipeline = create(:ci_pipeline, project: project, sha: project.commit.id, status: 'failed')
+
+ render
+
+ expect(rendered).to have_text("Pipeline ##{third_pipeline.id} for #{Commit.truncate_sha(project.commit.sha)} failed")
+ end
+end
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index c8a3d02d8fd..889d9a38887 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -5,7 +5,7 @@ describe 'projects/issues/_related_branches' do
let(:project) { create(:project) }
let(:branch) { project.repository.find_branch('feature') }
- let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.target.id, ref: 'feature') }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: branch.dereferenced_target.id, ref: 'feature') }
before do
assign(:project, project)
diff --git a/spec/workers/project_cache_worker_spec.rb b/spec/workers/project_cache_worker_spec.rb
index f5b60b90d11..bfa8c0ff2c6 100644
--- a/spec/workers/project_cache_worker_spec.rb
+++ b/spec/workers/project_cache_worker_spec.rb
@@ -5,6 +5,26 @@ describe ProjectCacheWorker do
subject { described_class.new }
+ describe '.perform_async' do
+ it 'schedules the job when no lease exists' do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
+ and_return(false)
+
+ expect_any_instance_of(described_class).to receive(:perform)
+
+ described_class.perform_async(project.id)
+ end
+
+ it 'does not schedule the job when a lease exists' do
+ allow_any_instance_of(Gitlab::ExclusiveLease).to receive(:exists?).
+ and_return(true)
+
+ expect_any_instance_of(described_class).not_to receive(:perform)
+
+ described_class.perform_async(project.id)
+ end
+ end
+
describe '#perform' do
context 'when an exclusive lease can be obtained' do
before do
diff --git a/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
new file mode 100644
index 00000000000..6d42946de38
--- /dev/null
+++ b/spec/workers/remove_unreferenced_lfs_objects_worker_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe RemoveUnreferencedLfsObjectsWorker do
+ let(:worker) { RemoveUnreferencedLfsObjectsWorker.new }
+
+ describe '#perform' do
+ let!(:unreferenced_lfs_object1) { create(:lfs_object, oid: '1') }
+ let!(:unreferenced_lfs_object2) { create(:lfs_object, oid: '2') }
+ let!(:project1) { create(:empty_project, lfs_enabled: true) }
+ let!(:project2) { create(:empty_project, lfs_enabled: true) }
+ let!(:referenced_lfs_object1) { create(:lfs_object, oid: '3') }
+ let!(:referenced_lfs_object2) { create(:lfs_object, oid: '4') }
+ let!(:lfs_objects_project1_1) do
+ create(:lfs_objects_project,
+ project: project1,
+ lfs_object: referenced_lfs_object1
+ )
+ end
+ let!(:lfs_objects_project2_1) do
+ create(:lfs_objects_project,
+ project: project2,
+ lfs_object: referenced_lfs_object1
+ )
+ end
+ let!(:lfs_objects_project1_2) do
+ create(:lfs_objects_project,
+ project: project1,
+ lfs_object: referenced_lfs_object2
+ )
+ end
+
+ it 'removes unreferenced lfs objects' do
+ worker.perform
+
+ expect(LfsObject.where(id: unreferenced_lfs_object1.id)).to be_empty
+ expect(LfsObject.where(id: unreferenced_lfs_object2.id)).to be_empty
+ end
+
+ it 'leaves referenced lfs objects' do
+ worker.perform
+
+ expect(referenced_lfs_object1.reload).to be_present
+ expect(referenced_lfs_object2.reload).to be_present
+ end
+
+ it 'removes unreferenced lfs objects after project removal' do
+ project1.destroy
+
+ worker.perform
+
+ expect(referenced_lfs_object1.reload).to be_present
+ expect(LfsObject.where(id: referenced_lfs_object2.id)).to be_empty
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/jquery.cookie.js b/vendor/assets/javascripts/jquery.cookie.js
deleted file mode 100644
index 6a3e394b403..00000000000
--- a/vendor/assets/javascripts/jquery.cookie.js
+++ /dev/null
@@ -1,41 +0,0 @@
-/**
- * jQuery Cookie plugin
- *
- * Copyright (c) 2010 Klaus Hartl (stilbuero.de)
- * Dual licensed under the MIT and GPL licenses:
- * http://www.opensource.org/licenses/mit-license.php
- * http://www.gnu.org/licenses/gpl.html
- *
- */
-jQuery.cookie = function (key, value, options) {
-
- // key and at least value given, set cookie...
- if (arguments.length > 1 && String(value) !== "[object Object]") {
- options = jQuery.extend({}, options);
-
- if (value === null || value === undefined) {
- options.expires = -1;
- }
-
- if (typeof options.expires === 'number') {
- var days = options.expires, t = options.expires = new Date();
- t.setDate(t.getDate() + days);
- }
-
- value = String(value);
-
- return (document.cookie = [
- encodeURIComponent(key), '=',
- options.raw ? value : encodeURIComponent(value),
- options.expires ? '; expires=' + options.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
- options.path ? '; path=' + options.path : '',
- options.domain ? '; domain=' + options.domain : '',
- options.secure ? '; secure' : ''
- ].join(''));
- }
-
- // key and possibly options given, get cookie...
- options = value || {};
- var result, decode = options.raw ? function (s) { return s; } : decodeURIComponent;
- return (result = new RegExp('(?:^|; )' + encodeURIComponent(key) + '=([^;]*)').exec(document.cookie)) ? decode(result[1]) : null;
-};
diff --git a/vendor/assets/javascripts/js.cookie.js b/vendor/assets/javascripts/js.cookie.js
new file mode 100644
index 00000000000..92dbba162c4
--- /dev/null
+++ b/vendor/assets/javascripts/js.cookie.js
@@ -0,0 +1,156 @@
+/*!
+ * JavaScript Cookie v2.1.3
+ * https://github.com/js-cookie/js-cookie
+ *
+ * Copyright 2006, 2015 Klaus Hartl & Fagner Brack
+ * Released under the MIT license
+ */
+;(function (factory) {
+ var registeredInModuleLoader = false;
+ if (typeof define === 'function' && define.amd) {
+ define(factory);
+ registeredInModuleLoader = true;
+ }
+ if (typeof exports === 'object') {
+ module.exports = factory();
+ registeredInModuleLoader = true;
+ }
+ if (!registeredInModuleLoader) {
+ var OldCookies = window.Cookies;
+ var api = window.Cookies = factory();
+ api.noConflict = function () {
+ window.Cookies = OldCookies;
+ return api;
+ };
+ }
+}(function () {
+ function extend () {
+ var i = 0;
+ var result = {};
+ for (; i < arguments.length; i++) {
+ var attributes = arguments[ i ];
+ for (var key in attributes) {
+ result[key] = attributes[key];
+ }
+ }
+ return result;
+ }
+
+ function init (converter) {
+ function api (key, value, attributes) {
+ var result;
+ if (typeof document === 'undefined') {
+ return;
+ }
+
+ // Write
+
+ if (arguments.length > 1) {
+ attributes = extend({
+ path: '/'
+ }, api.defaults, attributes);
+
+ if (typeof attributes.expires === 'number') {
+ var expires = new Date();
+ expires.setMilliseconds(expires.getMilliseconds() + attributes.expires * 864e+5);
+ attributes.expires = expires;
+ }
+
+ try {
+ result = JSON.stringify(value);
+ if (/^[\{\[]/.test(result)) {
+ value = result;
+ }
+ } catch (e) {}
+
+ if (!converter.write) {
+ value = encodeURIComponent(String(value))
+ .replace(/%(23|24|26|2B|3A|3C|3E|3D|2F|3F|40|5B|5D|5E|60|7B|7D|7C)/g, decodeURIComponent);
+ } else {
+ value = converter.write(value, key);
+ }
+
+ key = encodeURIComponent(String(key));
+ key = key.replace(/%(23|24|26|2B|5E|60|7C)/g, decodeURIComponent);
+ key = key.replace(/[\(\)]/g, escape);
+
+ return (document.cookie = [
+ key, '=', value,
+ attributes.expires ? '; expires=' + attributes.expires.toUTCString() : '', // use expires attribute, max-age is not supported by IE
+ attributes.path ? '; path=' + attributes.path : '',
+ attributes.domain ? '; domain=' + attributes.domain : '',
+ attributes.secure ? '; secure' : ''
+ ].join(''));
+ }
+
+ // Read
+
+ if (!key) {
+ result = {};
+ }
+
+ // To prevent the for loop in the first place assign an empty array
+ // in case there are no cookies at all. Also prevents odd result when
+ // calling "get()"
+ var cookies = document.cookie ? document.cookie.split('; ') : [];
+ var rdecode = /(%[0-9A-Z]{2})+/g;
+ var i = 0;
+
+ for (; i < cookies.length; i++) {
+ var parts = cookies[i].split('=');
+ var cookie = parts.slice(1).join('=');
+
+ if (cookie.charAt(0) === '"') {
+ cookie = cookie.slice(1, -1);
+ }
+
+ try {
+ var name = parts[0].replace(rdecode, decodeURIComponent);
+ cookie = converter.read ?
+ converter.read(cookie, name) : converter(cookie, name) ||
+ cookie.replace(rdecode, decodeURIComponent);
+
+ if (this.json) {
+ try {
+ cookie = JSON.parse(cookie);
+ } catch (e) {}
+ }
+
+ if (key === name) {
+ result = cookie;
+ break;
+ }
+
+ if (!key) {
+ result[name] = cookie;
+ }
+ } catch (e) {}
+ }
+
+ return result;
+ }
+
+ api.set = api;
+ api.get = function (key) {
+ return api.call(api, key);
+ };
+ api.getJSON = function () {
+ return api.apply({
+ json: true
+ }, [].slice.call(arguments));
+ };
+ api.defaults = {};
+
+ api.remove = function (key, attributes) {
+ api(key, '', extend(attributes, {
+ expires: -1
+ }));
+ };
+
+ api.withConverter = init;
+
+ return api;
+ }
+
+ return init(function () {});
+})); \ No newline at end of file