summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2016-08-22 11:34:41 +0300
committerDmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>2016-08-22 11:34:41 +0300
commit9329436deb3931d28c28b41de56173ef8acd63b5 (patch)
tree7557875233d3b506c2496599db1814afc7558c7c
parent6db65143db5003f74ddb1c9868a3d852e5661a0a (diff)
parentfb84439a92e759ff90811e98f6abf6bdbb3e6d55 (diff)
downloadgitlab-ce-9329436deb3931d28c28b41de56173ef8acd63b5.tar.gz
Merge branch 'master' into dz-merge-request-version
Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
-rw-r--r--CHANGELOG28
-rw-r--r--CONTRIBUTING.md3
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--app/assets/images/koding-logo.svg8
-rw-r--r--app/assets/javascripts/application.js6
-rw-r--r--app/assets/javascripts/blob_edit/blob_edit_bundle.js12
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js (renamed from app/assets/javascripts/blob/edit_blob.js)0
-rw-r--r--app/assets/javascripts/boards/boards_bundle.js.es62
-rw-r--r--app/assets/javascripts/boards/components/board.js.es66
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es66
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es624
-rw-r--r--app/assets/javascripts/boards/models/label.js.es61
-rw-r--r--[-rwxr-xr-x]app/assets/javascripts/boards/test_utils/simulate_drag.js0
-rw-r--r--app/assets/javascripts/dispatcher.js6
-rw-r--r--app/assets/javascripts/gl_dropdown.js93
-rw-r--r--app/assets/javascripts/issuable_form.js24
-rw-r--r--app/assets/javascripts/labels_select.js32
-rw-r--r--app/assets/javascripts/lib/ace.js2
-rw-r--r--app/assets/javascripts/member_expiration_date.js32
-rw-r--r--app/assets/javascripts/project.js5
-rw-r--r--app/assets/javascripts/project_members.js3
-rw-r--r--app/assets/javascripts/protected_branch_access_dropdown.js.es68
-rw-r--r--app/assets/javascripts/protected_branch_create.js.es64
-rw-r--r--app/assets/javascripts/protected_branch_edit.js.es63
-rw-r--r--app/assets/javascripts/search_autocomplete.js18
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js12
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss9
-rw-r--r--app/assets/stylesheets/framework/files.scss5
-rw-r--r--app/assets/stylesheets/framework/gfm.scss2
-rw-r--r--app/assets/stylesheets/framework/modal.scss1
-rw-r--r--app/assets/stylesheets/framework/nav.scss1
-rw-r--r--app/assets/stylesheets/framework/selects.scss3
-rw-r--r--app/assets/stylesheets/framework/typography.scss10
-rw-r--r--app/assets/stylesheets/pages/boards.scss43
-rw-r--r--app/assets/stylesheets/pages/builds.scss5
-rw-r--r--app/assets/stylesheets/pages/commit.scss9
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss7
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss7
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss15
-rw-r--r--app/assets/stylesheets/pages/projects.scss26
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/dashboard/todos_controller.rb1
-rw-r--r--app/controllers/groups/group_members_controller.rb9
-rw-r--r--app/controllers/koding_controller.rb15
-rw-r--r--app/controllers/projects/boards/issues_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb7
-rw-r--r--app/controllers/projects/group_links_controller.rb4
-rw-r--r--app/controllers/projects/project_members_controller.rb9
-rw-r--r--app/finders/move_to_project_finder.rb6
-rw-r--r--app/finders/todos_finder.rb6
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb8
-rw-r--r--app/helpers/ci_status_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb9
-rw-r--r--app/helpers/projects_helper.rb54
-rw-r--r--app/helpers/time_helper.rb17
-rw-r--r--app/models/ability.rb48
-rw-r--r--app/models/application_setting.rb6
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb10
-rw-r--r--app/models/commit.rb2
-rw-r--r--app/models/commit_status.rb13
-rw-r--r--app/models/concerns/expirable.rb15
-rw-r--r--app/models/concerns/issuable.rb19
-rw-r--r--app/models/concerns/note_on_diff.rb4
-rw-r--r--app/models/concerns/sortable.rb14
-rw-r--r--app/models/concerns/statuseable.rb15
-rw-r--r--app/models/diff_note.rb7
-rw-r--r--app/models/discussion.rb1
-rw-r--r--app/models/group.rb24
-rw-r--r--app/models/legacy_diff_note.rb4
-rw-r--r--app/models/member.rb4
-rw-r--r--app/models/members/project_member.rb10
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/project_group_link.rb4
-rw-r--r--app/models/project_team.rb13
-rw-r--r--app/models/repository.rb10
-rw-r--r--app/models/todo.rb19
-rw-r--r--app/services/members/authorized_destroy_service.rb19
-rw-r--r--app/services/members/destroy_service.rb7
-rw-r--r--app/services/notification_service.rb2
-rw-r--r--app/views/admin/application_settings/_form.html.haml19
-rw-r--r--app/views/admin/builds/_build.html.haml2
-rw-r--r--app/views/dashboard/todos/index.html.haml19
-rw-r--r--app/views/discussions/_diff_with_notes.html.haml2
-rw-r--r--app/views/groups/group_members/_new_group_member.html.haml9
-rw-r--r--app/views/groups/group_members/update.js.haml1
-rw-r--r--app/views/koding/index.html.haml9
-rw-r--r--app/views/layouts/koding.html.haml5
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml5
-rw-r--r--app/views/layouts/nav/_project.html.haml2
-rw-r--r--app/views/projects/blob/edit.html.haml9
-rw-r--r--app/views/projects/blob/new.html.haml9
-rw-r--r--app/views/projects/boards/components/_board.html.haml1
-rw-r--r--app/views/projects/boards/components/_card.html.haml1
-rw-r--r--app/views/projects/branches/_branch.html.haml4
-rw-r--r--app/views/projects/buttons/_koding.html.haml7
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml4
-rw-r--r--app/views/projects/commit/_change.html.haml4
-rw-r--r--app/views/projects/commit/_commit_box.html.haml8
-rw-r--r--app/views/projects/group_links/index.html.haml11
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/_show.html.haml3
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml2
-rw-r--r--app/views/projects/pipelines/_info.html.haml2
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml9
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/project_members/update.js.haml1
-rw-r--r--app/views/projects/protected_branches/_create_protected_branch.html.haml16
-rw-r--r--app/views/projects/releases/edit.html.haml16
-rw-r--r--app/views/projects/show.html.haml4
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/icons/_icon_play.svg1
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml2
-rw-r--r--app/views/shared/members/_member.html.haml20
-rw-r--r--app/views/shared/snippets/_form.html.haml9
-rw-r--r--app/workers/emails_on_push_worker.rb4
-rw-r--r--app/workers/remove_expired_group_links_worker.rb7
-rw-r--r--app/workers/remove_expired_members_worker.rb13
-rw-r--r--config/application.rb2
-rw-r--r--config/initializers/1_settings.rb6
-rw-r--r--config/routes.rb5
-rw-r--r--db/fixtures/development/14_pipelines.rb (renamed from db/fixtures/development/14_builds.rb)99
-rw-r--r--db/migrate/20160801163421_add_expires_at_to_member.rb29
-rw-r--r--db/migrate/20160817133006_add_koding_to_application_settings.rb10
-rw-r--r--db/migrate/20160818205718_add_expires_at_to_project_group_links.rb29
-rw-r--r--db/migrate/20160819221631_add_index_to_note_discussion_id.rb14
-rw-r--r--db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb12
-rw-r--r--db/schema.rb9
-rw-r--r--doc/api/members.md5
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/integration/README.md1
-rw-r--r--doc/integration/img/koding_build-in-progress.pngbin0 -> 70949 bytes
-rw-r--r--doc/integration/img/koding_build-logs.pngbin0 -> 263623 bytes
-rw-r--r--doc/integration/img/koding_build-success.pngbin0 -> 304666 bytes
-rw-r--r--doc/integration/img/koding_commit-koding.yml.pngbin0 -> 302703 bytes
-rw-r--r--doc/integration/img/koding_different-stack-on-mr-try.pngbin0 -> 333649 bytes
-rw-r--r--doc/integration/img/koding_edit-on-ide.pngbin0 -> 330880 bytes
-rw-r--r--doc/integration/img/koding_enable-koding.pngbin0 -> 73499 bytes
-rw-r--r--doc/integration/img/koding_landing.pngbin0 -> 268455 bytes
-rw-r--r--doc/integration/img/koding_open-gitlab-from-koding.pngbin0 -> 32559 bytes
-rw-r--r--doc/integration/img/koding_run-in-ide.pngbin0 -> 65465 bytes
-rw-r--r--doc/integration/img/koding_run-mr-in-ide.pngbin0 -> 339759 bytes
-rw-r--r--doc/integration/img/koding_set-up-ide.pngbin0 -> 207481 bytes
-rw-r--r--doc/integration/img/koding_stack-import.pngbin0 -> 500352 bytes
-rw-r--r--doc/integration/img/koding_start-build.pngbin0 -> 105253 bytes
-rw-r--r--doc/integration/koding-usage.md122
-rw-r--r--doc/integration/koding.md239
-rw-r--r--doc/update/8.10-to-8.11.md2
-rw-r--r--doc/workflow/share_projects_with_other_groups.md18
-rw-r--r--features/steps/group/members.rb4
-rw-r--r--features/steps/project/source/browse_files.rb8
-rw-r--r--features/steps/project/team_management.rb4
-rw-r--r--lib/api/entities.rb4
-rw-r--r--lib/api/members.rb9
-rw-r--r--lib/extracts_path.rb12
-rw-r--r--lib/gitlab/badge/coverage/report.rb3
-rw-r--r--lib/gitlab/current_settings.rb1
-rw-r--r--lib/gitlab/diff/position.rb18
-rw-r--r--lib/gitlab/email/handler.rb3
-rw-r--r--lib/gitlab/utils.rb2
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb50
-rw-r--r--spec/features/boards/boards_spec.rb12
-rw-r--r--spec/features/issues_spec.rb2
-rw-r--r--spec/features/projects/badges/coverage_spec.rb41
-rw-r--r--spec/features/projects/branches/delete_spec.rb24
-rw-r--r--spec/features/projects/branches_spec.rb2
-rw-r--r--spec/features/projects/commits/cherry_pick_spec.rb31
-rw-r--r--spec/features/projects/group_links_spec.rb32
-rw-r--r--spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb45
-rw-r--r--spec/features/protected_branches/access_control_ce_spec.rb71
-rw-r--r--spec/features/protected_branches_spec.rb71
-rw-r--r--spec/features/security/dashboard_access_spec.rb14
-rw-r--r--spec/features/todos/todos_sorting_spec.rb67
-rw-r--r--spec/finders/move_to_project_finder_spec.rb22
-rw-r--r--spec/finders/todos_finder_spec.rb70
-rw-r--r--spec/fixtures/api/schemas/issue.json40
-rw-r--r--spec/helpers/issuables_helper_spec.rb16
-rw-r--r--spec/helpers/time_helper_spec.rb16
-rw-r--r--spec/javascripts/fixtures/gl_dropdown.html.haml16
-rw-r--r--spec/javascripts/fixtures/issue_sidebar_label.html.haml16
-rw-r--r--spec/javascripts/gl_dropdown_spec.js.es6119
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es689
-rw-r--r--spec/javascripts/search_autocomplete_spec.js8
-rw-r--r--spec/lib/extracts_path_spec.rb21
-rw-r--r--spec/lib/gitlab/badge/coverage/report_spec.rb67
-rw-r--r--spec/lib/gitlab/diff/position_spec.rb42
-rw-r--r--spec/lib/gitlab/email/handler/create_issue_handler_spec.rb2
-rw-r--r--spec/mailers/notify_spec.rb14
-rw-r--r--spec/models/ability_spec.rb64
-rw-r--r--spec/models/broadcast_message_spec.rb2
-rw-r--r--spec/models/ci/pipeline_spec.rb14
-rw-r--r--spec/models/member_spec.rb14
-rw-r--r--spec/models/network/graph_spec.rb12
-rw-r--r--spec/models/project_spec.rb2
-rw-r--r--spec/models/repository_spec.rb8
-rw-r--r--spec/requests/api/members_spec.rb6
-rw-r--r--spec/routing/routing_spec.rb9
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb46
-rw-r--r--spec/services/notification_service_spec.rb40
-rw-r--r--spec/spec_helper.rb1
-rw-r--r--spec/workers/emails_on_push_worker_spec.rb36
-rw-r--r--spec/workers/remove_expired_group_links_worker_spec.rb24
-rw-r--r--spec/workers/remove_expired_members_worker_spec.rb58
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/Chart.js0
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/autosize.js0
-rw-r--r--[-rwxr-xr-x]vendor/assets/javascripts/jquery.scrollTo.js0
211 files changed, 2637 insertions, 563 deletions
diff --git a/CHANGELOG b/CHANGELOG
index 7ea2631f9f4..c6a4fe5f5b8 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,12 +1,15 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.11.0 (unreleased)
+ - Use test coverage value from the latest successful pipeline in badge. !5862
- Add test coverage report badge. !5708
- Remove the http_parser.rb dependency by removing the tinder gem. !5758 (tbalthazar)
+ - Add Koding (online IDE) integration
- Ability to specify branches for Pivotal Tracker integration (Egor Lynko)
- Fix don't pass a local variable called `i` to a partial. !20510 (herminiotorres)
- Add delimiter to project stars and forks count (ClemMakesApps)
- Fix rename `add_users_into_project` and `projects_ids`. !20512 (herminiotorres)
+ - Fix adding line comments on the initial commit to a repo !5900
- Fix the title of the toggle dropdown button. !5515 (herminiotorres)
- Rename `markdown_preview` routes to `preview_markdown`. (Christopher Bartz)
- Update to Ruby 2.3.1. !4948
@@ -18,13 +21,17 @@ v 8.11.0 (unreleased)
- API: Endpoints for enabling and disabling deploy keys
- API: List access requests, request access, approve, and deny access requests to a project or a group. !4833
- Use long options for curl examples in documentation !5703 (winniehell)
+ - Added tooltip listing label names to the labels value in the collapsed issuable sidebar
- Remove magic comments (`# encoding: UTF-8`) from Ruby files. !5456 (winniehell)
+ - Fix badge count alignment (ClemMakesApps)
- GitLab Performance Monitoring can now track custom events such as the number of tags pushed to a repository
- Add support for relative links starting with ./ or / to RelativeLinkFilter (winniehell)
- Allow naming U2F devices !5833
- Ignore URLs starting with // in Markdown links !5677 (winniehell)
- Fix CI status icon link underline (ClemMakesApps)
- The Repository class is now instrumented
+ - Fix commit mention font inconsistency (ClemMakesApps)
+ - Do not escape URI when extracting path !5878 (winniehell)
- Fix filter label tooltip HTML rendering (ClemMakesApps)
- Cache the commit author in RequestStore to avoid extra lookups in PostReceive
- Expand commit message width in repo view (ClemMakesApps)
@@ -34,9 +41,11 @@ v 8.11.0 (unreleased)
- API: Add deployment endpoints
- API: Add Play endpoint on Builds
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
+ - Show wall clock time when showing a pipeline. !5734
- Show member roles to all users on members page
- Project.visible_to_user is instrumented again
- Fix awardable button mutuality loading spinners (ClemMakesApps)
+ - Sort todos by date and priority
- Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
- Optimize maximum user access level lookup in loading of notes
- Send notification emails to users newly mentioned in issue and MR edits !5800
@@ -55,6 +64,7 @@ v 8.11.0 (unreleased)
- Enforce 2FA restrictions on API authentication endpoints !5820
- Limit git rev-list output count to one in forced push check
- Show deployment status on merge requests with external URLs
+ - Fix branch title trailing space on hover (ClemMakesApps)
- Clean up unused routes (Josef Strzibny)
- Fix issue on empty project to allow developers to only push to protected branches if given permission
- API: Add enpoints for pipelines
@@ -71,6 +81,7 @@ v 8.11.0 (unreleased)
- Fix devise deprecation warnings.
- Check for 2FA when using Git over HTTP and only allow PersonalAccessTokens as password in that case !5764
- Update version_sorter and use new interface for faster tag sorting
+ - Load branches asynchronously in Cherry Pick and Revert dialogs.
- Optimize checking if a user has read access to a list of issues !5370
- Store all DB secrets in secrets.yml, under descriptive names !5274
- Fix syntax highlighting in file editor
@@ -79,7 +90,6 @@ v 8.11.0 (unreleased)
- Add archived badge to project list !5798
- Add simple identifier to public SSH keys (muteor)
- Admin page now references docs instead of a specific file !5600 (AnAverageHuman)
- - Add a way to send an email and create an issue based on private personal token. Find the email address from issues page. !3363
- Fix filter input alignment (ClemMakesApps)
- Include old revision in merge request update hooks (Ben Boeckel)
- Add build event color in HipChat messages (David Eisner)
@@ -105,12 +115,14 @@ v 8.11.0 (unreleased)
- Fix search for notes which belongs to deleted objects
- Allow Akismet to be trained by submitting issues as spam or ham !5538
- Add GitLab Workhorse version to admin dashboard (Katarzyna Kobierska Ula Budziszewska)
+ - Fix spacing and vertical alignment on build status icon on commits page (ClemMakesApps)
- Allow branch names ending with .json for graph and network page !5579 (winniehell)
- Add the `sprockets-es6` gem
- Improve OAuth2 client documentation (muteor)
- Fix diff comments inverted toggle bug (ClemMakesApps)
- Multiple trigger variables show in separate lines (Katarzyna Kobierska Ula Budziszewska)
- Profile requests when a header is passed
+ - Fix button missing type (ClemMakesApps)
- Avoid calculation of line_code and position for _line partial when showing diff notes on discussion tab.
- Speedup DiffNote#active? on discussions, preloading noteables and avoid touching git repository to return diff_refs when possible
- Add commit stats in commit api. !5517 (dixpac)
@@ -119,14 +131,17 @@ v 8.11.0 (unreleased)
- edit_blob_link will use blob passed onto the options parameter
- Make error pages responsive (Takuya Noguchi)
- The performance of the project dropdown used for moving issues has been improved
+ - Move to project dropdown with infinite scroll for better performance
- Fix skip_repo parameter being ignored when destroying a namespace
- Add all builds into stage/job dropdowns on builds page
- Change requests_profiles resource constraint to catch virtually any file
- Bump gitlab_git to lazy load compare commits
- Reduce number of queries made for merge_requests/:id/diffs
+ - Add the option to set the expiration date for the project membership when giving a user access to a project. !5599 (Adam Niedzielski)
- Sensible state specific default sort order for issues and merge requests !5453 (tomb0y)
- Fix bug where destroying a namespace would not always destroy projects
- Fix RequestProfiler::Middleware error when code is reloaded in development
+ - Allow horizontal scrolling of code blocks in issue body
- Catch what warden might throw when profiling requests to re-throw it
- Avoid commit lookup on diff_helper passing existing local variable to the helper method
- Add description to new_issue email and new_merge_request_email in text/plain content type. !5663 (dixpac)
@@ -141,6 +156,7 @@ v 8.11.0 (unreleased)
- Each `File::exists?` replaced to `File::exist?` because of deprecate since ruby version 2.2.0
- Add auto-completition in pipeline (Katarzyna Kobierska Ula Budziszewska)
- Add pipelines tab to merge requests
+ - Fix notification_service argument error of declined invitation emails
- Fix a memory leak caused by Banzai::Filter::SanitizationFilter
- Speed up todos queries by limiting the projects set we join with
- Ensure file editing in UI does not overwrite commited changes without warning user
@@ -148,6 +164,10 @@ v 8.11.0 (unreleased)
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
+v 8.10.7
+ - Upgrade Hamlit to 2.6.1. !5873
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.10.6
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Restore "Largest repository" sort option on Admin > Projects page. !5797
@@ -371,6 +391,9 @@ v 8.10.0
- Fix migration corrupting import data for old version upgrades
- Show tooltip on GitLab export link in new project page
+v 8.9.8
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.9.7
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
- Require administrator privileges to perform a project import.
@@ -640,6 +663,9 @@ v 8.9.0
- Add tooltip to pin/unpin navbar
- Add new sub nav style to Wiki and Graphs sub navigation
+v 8.8.9
+ - Upgrade Doorkeeper to 4.2.0. !5881
+
v 8.8.8
- Upgrade Rails to 4.2.7.1 for security fixes. !5781
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index fbc8e15bebf..d8093a61b4c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -387,7 +387,8 @@ description area. Copy-paste it to retain the markdown format.
1. The change is as small as possible
1. Include proper tests and make all tests pass (unless it contains a test
- exposing a bug in existing code)
+ exposing a bug in existing code). Every new class should have corresponding
+ unit tests, even if the class is exercised at a higher level, such as a feature test.
1. If you suspect a failing CI build is unrelated to your contribution, you may
try and restart the failing CI job or ask a developer to fix the
aforementioned failing test
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 619b5376684..18091983f59 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.3.3
+3.4.0
diff --git a/app/assets/images/koding-logo.svg b/app/assets/images/koding-logo.svg
new file mode 100644
index 00000000000..ad89d684d94
--- /dev/null
+++ b/app/assets/images/koding-logo.svg
@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="utf-8"?>
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 14">
+ <g fill="#d6d7d9">
+ <path d="M8.7 0L5.3.3l3.2 6.8-3.2 6.6 3.5.3L12 6.9z"/>
+ <ellipse cx="1.7" cy="11.1" rx="1.7" ry="1.7"/>
+ <ellipse cx="1.7" cy="5.6" rx="1.7" ry="1.7"/>
+ </g>
+</svg> \ No newline at end of file
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index a122fa2d637..fc354dfd677 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -26,8 +26,6 @@
/*= require bootstrap/tooltip */
/*= require bootstrap/popover */
/*= require select2 */
-/*= require ace-rails-ap */
-/*= require ace/ext-searchbox */
/*= require underscore */
/*= require dropzone */
/*= require mousetrap */
@@ -153,7 +151,9 @@
});
});
$('.remove-row').bind('ajax:success', function() {
- return $(this).closest('li').fadeOut();
+ $(this).tooltip('destroy')
+ .closest('li')
+ .fadeOut();
});
$('.js-remove-tr').bind('ajax:before', function() {
return $(this).hide();
diff --git a/app/assets/javascripts/blob_edit/blob_edit_bundle.js b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
new file mode 100644
index 00000000000..2afef43f3d6
--- /dev/null
+++ b/app/assets/javascripts/blob_edit/blob_edit_bundle.js
@@ -0,0 +1,12 @@
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var url = $(".js-edit-blob-form").data("relative-url-root");
+ url += $(".js-edit-blob-form").data("assets-prefix");
+
+ var blob = new EditBlob(url, $('.js-edit-blob-form').data('blob-language'));
+ new NewCommitForm($('.js-edit-blob-form'));
+ });
+
+}).call(this);
diff --git a/app/assets/javascripts/blob/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index 649c79daee8..649c79daee8 100644
--- a/app/assets/javascripts/blob/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
diff --git a/app/assets/javascripts/boards/boards_bundle.js.es6 b/app/assets/javascripts/boards/boards_bundle.js.es6
index 2c65d4427be..a612cf0f1ae 100644
--- a/app/assets/javascripts/boards/boards_bundle.js.es6
+++ b/app/assets/javascripts/boards/boards_bundle.js.es6
@@ -38,7 +38,7 @@ $(() => {
ready () {
Store.disabled = this.disabled;
gl.boardService.all()
- .then((resp) => {
+ .then((resp) => {
resp.json().forEach((board) => {
const list = Store.addList(board);
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index e17784e7948..d7f4107cb02 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -55,7 +55,7 @@
draggable: '.is-draggable',
handle: '.js-board-handle',
onEnd: (e) => {
- document.body.classList.remove('is-dragging');
+ gl.issueBoards.onEnd();
if (e.newIndex !== undefined && e.oldIndex !== e.newIndex) {
const order = this.sortable.toArray(),
@@ -72,10 +72,6 @@
}
});
- if (bp.getBreakpointSize() === 'xs') {
- options.handle = '.js-board-drag-handle';
- }
-
this.sortable = Sortable.create(this.$el.parentNode, options);
},
beforeDestroy () {
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 1503d14c508..a6644e9eb8c 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -63,6 +63,8 @@
Store.moving.issue = card.issue;
Store.moving.list = card.list;
+
+ gl.issueBoards.onStart();
},
onAdd: (e) => {
gl.issueBoards.BoardsStore.moveIssueToList(Store.moving.list, this.list, Store.moving.issue);
@@ -72,10 +74,6 @@
}
});
- if (bp.getBreakpointSize() === 'xs') {
- options.handle = '.js-card-drag-handle';
- }
-
this.sortable = Sortable.create(this.$els.list, options);
// Scroll event on list to load more
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index b7afe4897b6..44addb3ea98 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -2,6 +2,19 @@
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
+ gl.issueBoards.onStart = () => {
+ $('.has-tooltip').tooltip('hide')
+ .tooltip('disable');
+ document.body.classList.add('is-dragging');
+ };
+
+ gl.issueBoards.onEnd = () => {
+ $('.has-tooltip').tooltip('enable');
+ document.body.classList.remove('is-dragging');
+ };
+
+ gl.issueBoards.touchEnabled = ('ontouchstart' in window) || window.DocumentTouch && document instanceof DocumentTouch;
+
gl.issueBoards.getBoardSortableDefaultOptions = (obj) => {
let defaultSortOptions = {
forceFallback: true,
@@ -9,14 +22,11 @@
fallbackOnBody: true,
ghostClass: 'is-ghost',
filter: '.has-tooltip',
- scrollSensitivity: 100,
+ delay: gl.issueBoards.touchEnabled ? 100 : 0,
+ scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
- onStart () {
- document.body.classList.add('is-dragging');
- },
- onEnd () {
- document.body.classList.remove('is-dragging');
- }
+ onStart: gl.issueBoards.onStart,
+ onEnd: gl.issueBoards.onEnd
}
Object.keys(obj).forEach((key) => { defaultSortOptions[key] = obj[key]; });
diff --git a/app/assets/javascripts/boards/models/label.js.es6 b/app/assets/javascripts/boards/models/label.js.es6
index e81e91fe972..583829552cd 100644
--- a/app/assets/javascripts/boards/models/label.js.es6
+++ b/app/assets/javascripts/boards/models/label.js.es6
@@ -3,6 +3,7 @@ class ListLabel {
this.id = obj.id;
this.title = obj.title;
this.color = obj.color;
+ this.textColor = obj.text_color;
this.description = obj.description;
this.priority = (obj.priority !== null) ? obj.priority : Infinity;
}
diff --git a/app/assets/javascripts/boards/test_utils/simulate_drag.js b/app/assets/javascripts/boards/test_utils/simulate_drag.js
index 75f8b730195..75f8b730195 100755..100644
--- a/app/assets/javascripts/boards/test_utils/simulate_drag.js
+++ b/app/assets/javascripts/boards/test_utils/simulate_drag.js
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 74c4ab563f9..ba64d2bcf0b 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -20,6 +20,9 @@
path = page.split(':');
shortcut_handler = null;
switch (page) {
+ case 'projects:boards:show':
+ shortcut_handler = new ShortcutsNavigation();
+ break;
case 'projects:issues:index':
Issuable.init();
new IssuableBulkActions();
@@ -126,10 +129,12 @@
new NotificationsDropdown();
break;
case 'groups:group_members:index':
+ new gl.MemberExpirationDate();
new GroupMembers();
new UsersSelect();
break;
case 'projects:project_members:index':
+ new gl.MemberExpirationDate();
new ProjectMembers();
new UsersSelect();
break;
@@ -171,6 +176,7 @@
new BuildArtifacts();
break;
case 'projects:group_links:index':
+ new gl.MemberExpirationDate();
new GroupsSelect();
break;
case 'search:show':
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index d3394fae3f9..24abea0d30d 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -31,9 +31,8 @@
this.input
.on('keydown', function (e) {
var keyCode = e.which;
-
if (keyCode === 13) {
- e.preventDefault()
+ e.preventDefault();
}
})
.on('keyup', function(e) {
@@ -111,9 +110,9 @@
matches = fuzzaldrinPlus.match($el.text().trim(), search_text);
if (!$el.is('.dropdown-header')) {
if (matches.length) {
- return $el.show();
+ return $el.show().removeClass('option-hidden');
} else {
- return $el.hide();
+ return $el.hide().addClass('option-hidden');
}
}
});
@@ -179,7 +178,7 @@
})();
GitLabDropdown = (function() {
- var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, currentIndex;
+ var ACTIVE_CLASS, FILTER_INPUT, INDETERMINATE_CLASS, LOADING_CLASS, PAGE_TWO_CLASS, NON_SELECTABLE_CLASSES, SELECTABLE_CLASSES, currentIndex;
LOADING_CLASS = "is-loading";
@@ -191,6 +190,12 @@
currentIndex = -1;
+ NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link, .option-hidden';
+
+ SELECTABLE_CLASSES = ".dropdown-content li:not(" + NON_SELECTABLE_CLASSES + ")";
+
+ CURSOR_SELECT_SCROLL_PADDING = 5
+
FILTER_INPUT = '.dropdown-input .dropdown-input-field';
function GitLabDropdown(el1, options) {
@@ -213,6 +218,7 @@
if (this.options.data) {
if (_.isObject(this.options.data) && !_.isFunction(this.options.data)) {
this.fullData = this.options.data;
+ currentIndex = -1;
this.parseData(this.options.data);
} else {
this.remote = new GitLabDropdownRemote(this.options.data, {
@@ -240,7 +246,7 @@
keys: searchFields,
elements: (function(_this) {
return function() {
- selector = '.dropdown-content li:not(.divider)';
+ selector = '.dropdown-content li:not(' + NON_SELECTABLE_CLASSES + ')';
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
@@ -256,7 +262,7 @@
return function(data) {
_this.parseData(data);
if (_this.filterInput.val() !== '') {
- selector = '.dropdown-content li:not(.divider):visible';
+ selector = SELECTABLE_CLASSES;
if (_this.dropdown.find('.dropdown-toggle-page').length) {
selector = ".dropdown-page-one " + selector;
}
@@ -376,7 +382,7 @@
var $target;
if (this.options.multiSelect) {
$target = $(e.target);
- if (!$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
+ if ($target && !$target.hasClass('dropdown-menu-close') && !$target.hasClass('dropdown-menu-close-icon') && !$target.data('is-link')) {
e.stopPropagation();
return false;
} else {
@@ -387,7 +393,7 @@
GitLabDropdown.prototype.opened = function() {
var contentHtml;
- currentIndex = -1;
+ this.resetRows();
this.addArrowKeyEvent();
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
@@ -410,6 +416,7 @@
GitLabDropdown.prototype.hidden = function(e) {
var $input;
+ this.resetRows();
this.removeArrayKeyEvent();
$input = this.dropdown.find(".dropdown-input-field");
if (this.options.filterable) {
@@ -463,14 +470,15 @@
return "<li class='separator'></li>";
}
if (data.header != null) {
- return "<li class='dropdown-header'>" + data.header + "</li>";
+ return _.template('<li class="dropdown-header"><%- header %></li>')({ header: data.header });
}
if (this.options.renderRow) {
html = this.options.renderRow.call(this.options, data, this);
} else {
if (!selected) {
value = this.options.id ? this.options.id(data) : data.id;
- fieldName = this.options.fieldName;
+ fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName() : this.options.fieldName;
+
field = this.dropdown.parent().find("input[name='" + fieldName + "'][value='" + value + "']");
if (field.length) {
selected = true;
@@ -494,11 +502,16 @@
text = this.highlightTextMatches(text, this.filterInput.val());
}
if (group) {
- groupAttrs = "data-group='" + group + "' data-index='" + index + "'";
+ groupAttrs = 'data-group=' + group + ' data-index=' + index;
} else {
groupAttrs = '';
}
- html = "<li> <a href='" + url + "' " + groupAttrs + " class='" + cssClass + "'> " + text + " </a> </li>";
+ html = _.template('<li><a href="<%- url %>" <%- groupAttrs %> class="<%- cssClass %>"><%= text %></a></li>')({
+ url: url,
+ groupAttrs: groupAttrs,
+ cssClass: cssClass,
+ text: text
+ });
}
return html;
};
@@ -520,20 +533,8 @@
return html = "<li class='dropdown-menu-empty-link'> <a href='#' class='is-focused'> No matching results. </a> </li>";
};
- GitLabDropdown.prototype.highlightRow = function(index) {
- var selector;
- if (this.filterInput.val() !== "") {
- selector = '.dropdown-content li:first-child a';
- if (this.dropdown.find(".dropdown-toggle-page").length) {
- selector = ".dropdown-page-one .dropdown-content li:first-child a";
- }
- return this.getElement(selector).addClass('is-focused');
- }
- };
-
GitLabDropdown.prototype.rowClicked = function(el) {
var field, fieldName, groupName, isInput, selectedIndex, selectedObject, value;
- fieldName = this.options.fieldName;
isInput = $(this.el).is('input');
if (this.renderedData) {
groupName = el.data('group');
@@ -545,6 +546,7 @@
selectedObject = this.renderedData[selectedIndex];
}
}
+ fieldName = typeof this.options.fieldName === 'function' ? this.options.fieldName(selectedObject) : this.options.fieldName;
value = this.options.id ? this.options.id(selectedObject, el) : selectedObject.id;
if (isInput) {
field = $(this.el);
@@ -559,10 +561,9 @@
field.remove();
}
if (this.options.toggleLabel) {
- return this.updateLabel(selectedObject, el, this);
- } else {
- return selectedObject;
+ this.updateLabel(selectedObject, el, this);
}
+ return selectedObject;
} else if (el.hasClass(INDETERMINATE_CLASS)) {
el.addClass(ACTIVE_CLASS);
el.removeClass(INDETERMINATE_CLASS);
@@ -570,7 +571,7 @@
field.remove();
}
if (!field.length && fieldName) {
- this.addInput(fieldName, value);
+ this.addInput(fieldName, value, selectedObject);
}
return selectedObject;
} else {
@@ -589,7 +590,7 @@
}
if (value != null) {
if (!field.length && fieldName) {
- this.addInput(fieldName, value);
+ this.addInput(fieldName, value, selectedObject);
} else {
field.val(value).trigger('change');
}
@@ -598,24 +599,29 @@
}
};
- GitLabDropdown.prototype.addInput = function(fieldName, value) {
+ GitLabDropdown.prototype.addInput = function(fieldName, value, selectedObject) {
var $input;
$input = $('<input>').attr('type', 'hidden').attr('name', fieldName).val(value);
if (this.options.inputId != null) {
$input.attr('id', this.options.inputId);
}
+ if (selectedObject && selectedObject.type) {
+ $input.attr('data-type', selectedObject.type);
+ }
return this.dropdown.before($input);
};
GitLabDropdown.prototype.selectRowAtIndex = function(index) {
var $el, selector;
- selector = ".dropdown-content li:not(.divider,.dropdown-header,.separator):eq(" + index + ") a";
+ selector = SELECTABLE_CLASSES + ":eq(" + index + ") a";
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
$el = $(selector, this.dropdown);
if ($el.length) {
- return $el.first().trigger('click');
+ $el.first().trigger('click');
+ var href = $el.attr('href');
+ if (href && href !== '#') Turbolinks.visit(href);
}
};
@@ -623,7 +629,7 @@
var $input, ARROW_KEY_CODES, selector;
ARROW_KEY_CODES = [38, 40];
$input = this.dropdown.find(".dropdown-input-field");
- selector = '.dropdown-content li:not(.divider,.dropdown-header,.separator):visible';
+ selector = SELECTABLE_CLASSES;
if (this.dropdown.find(".dropdown-toggle-page").length) {
selector = ".dropdown-page-one " + selector;
}
@@ -651,7 +657,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
- return _this.selectRowAtIndex($('.is-focused', _this.dropdown).closest('li').index() - 1);
+ return _this.selectRowAtIndex(currentIndex);
}
};
})(this));
@@ -661,6 +667,11 @@
return $('body').off('keydown');
};
+ GitLabDropdown.prototype.resetRows = function resetRows() {
+ currentIndex = -1;
+ $('.is-focused', this.dropdown).removeClass('is-focused');
+ };
+
GitLabDropdown.prototype.highlightRowAtIndex = function($listItems, index) {
var $dropdownContent, $listItem, dropdownContentBottom, dropdownContentHeight, dropdownContentTop, dropdownScrollTop, listItemBottom, listItemHeight, listItemTop;
$('.is-focused', this.dropdown).removeClass('is-focused');
@@ -674,10 +685,14 @@
listItemHeight = $listItem.outerHeight();
listItemTop = $listItem.prop('offsetTop');
listItemBottom = listItemTop + listItemHeight;
- if (listItemBottom > dropdownContentBottom + dropdownScrollTop) {
- return $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom);
- } else if (listItemTop < dropdownContentTop + dropdownScrollTop) {
- return $dropdownContent.scrollTop(listItemTop - dropdownContentTop);
+ if (!index) {
+ $dropdownContent.scrollTop(0)
+ } else if (index === ($listItems.length - 1)) {
+ $dropdownContent.scrollTop($dropdownContent.prop('scrollHeight'));
+ } else if (listItemBottom > (dropdownContentBottom + dropdownScrollTop)) {
+ $dropdownContent.scrollTop(listItemBottom - dropdownContentBottom + CURSOR_SELECT_SCROLL_PADDING);
+ } else if (listItemTop < (dropdownContentTop + dropdownScrollTop)) {
+ return $dropdownContent.scrollTop(listItemTop - dropdownContentTop - CURSOR_SELECT_SCROLL_PADDING);
}
};
diff --git a/app/assets/javascripts/issuable_form.js b/app/assets/javascripts/issuable_form.js
index 297d4f029f0..b7f92ae9883 100644
--- a/app/assets/javascripts/issuable_form.js
+++ b/app/assets/javascripts/issuable_form.js
@@ -102,20 +102,34 @@
};
IssuableForm.prototype.initMoveDropdown = function() {
- var $moveDropdown;
+ var $moveDropdown, pageSize;
$moveDropdown = $('.js-move-dropdown');
if ($moveDropdown.length) {
+ pageSize = $moveDropdown.data('page-size');
return $('.js-move-dropdown').select2({
ajax: {
url: $moveDropdown.data('projects-url'),
- results: function(data) {
+ quietMillis: 125,
+ data: function(term, page, context) {
return {
- results: data
+ search: term,
+ offset_id: context
};
},
- data: function(query) {
+ results: function(data) {
+ var context,
+ more;
+
+ if (data.length >= pageSize)
+ more = true;
+
+ if (data[data.length - 1])
+ context = data[data.length - 1].id;
+
return {
- search: query
+ results: data,
+ more: more,
+ context: context
};
}
},
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 0526430989f..565dbeacdb3 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,7 +4,7 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
labelUrl = $dropdown.data('labels');
@@ -21,6 +21,7 @@
$block = $selectbox.closest('.block');
$form = $dropdown.closest('form');
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span');
+ $sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
if (issueUpdateURL != null) {
@@ -31,7 +32,11 @@
labelNoneHTMLTemplate = '<span class="no-value">None</span>';
}
- new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+ $sidebarLabelTooltip.tooltip();
+
+ if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
+ new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+ }
saveLabelData = function() {
var data, selected;
@@ -52,7 +57,7 @@
dataType: 'JSON',
data: data
}).done(function(data) {
- var labelCount, template;
+ var labelCount, template, labelTooltipTitle, labelTitles;
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
@@ -66,6 +71,27 @@
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
+
+ if (data.labels.length) {
+ labelTitles = data.labels.map(function(label) {
+ return label.title;
+ });
+
+ if (labelTitles.length > 5) {
+ labelTitles = labelTitles.slice(0, 5);
+ labelTitles.push('and ' + (data.labels.length - 5) + ' more');
+ }
+
+ labelTooltipTitle = labelTitles.join(', ');
+ } else {
+ labelTooltipTitle = '';
+ $sidebarLabelTooltip.tooltip('destroy');
+ }
+
+ $sidebarLabelTooltip
+ .attr('title', labelTooltipTitle)
+ .tooltip('fixTitle');
+
$('.has-tooltip', $value).tooltip({
container: 'body'
});
diff --git a/app/assets/javascripts/lib/ace.js b/app/assets/javascripts/lib/ace.js
new file mode 100644
index 00000000000..4cdf99cae72
--- /dev/null
+++ b/app/assets/javascripts/lib/ace.js
@@ -0,0 +1,2 @@
+/*= require ace-rails-ap */
+/*= require ace/ext-searchbox */
diff --git a/app/assets/javascripts/member_expiration_date.js b/app/assets/javascripts/member_expiration_date.js
new file mode 100644
index 00000000000..1935af491f7
--- /dev/null
+++ b/app/assets/javascripts/member_expiration_date.js
@@ -0,0 +1,32 @@
+(function() {
+ // Add datepickers to all `js-access-expiration-date` elements. If those elements are
+ // children of an element with the `clearable-input` class, and have a sibling
+ // `js-clear-input` element, then show that element when there is a value in the
+ // datepicker, and make clicking on that element clear the field.
+ //
+ gl.MemberExpirationDate = function() {
+ function toggleClearInput() {
+ $(this).closest('.clearable-input').toggleClass('has-value', $(this).val() !== '');
+ }
+
+ var inputs = $('.js-access-expiration-date');
+
+ inputs.datepicker({
+ dateFormat: 'yy-mm-dd',
+ minDate: 1,
+ onSelect: toggleClearInput
+ });
+
+ inputs.next('.js-clear-input').on('click', function(event) {
+ event.preventDefault();
+
+ var input = $(this).closest('.clearable-input').find('.js-access-expiration-date');
+ input.datepicker('setDate', null);
+ toggleClearInput.call(input);
+ });
+
+ inputs.on('blur', toggleClearInput);
+
+ inputs.each(toggleClearInput);
+ };
+}).call(this);
diff --git a/app/assets/javascripts/project.js b/app/assets/javascripts/project.js
index b97f6d22715..4e1de4dfb72 100644
--- a/app/assets/javascripts/project.js
+++ b/app/assets/javascripts/project.js
@@ -65,7 +65,8 @@
url: $dropdown.data('refs-url'),
data: {
ref: $dropdown.data('ref')
- }
+ },
+ dataType: "json"
}).done(function(refs) {
return callback(refs);
});
@@ -73,7 +74,7 @@
selectable: true,
filterable: true,
filterByText: true,
- fieldName: 'ref',
+ fieldName: $dropdown.data('field-name'),
renderRow: function(ref) {
var link;
if (ref.header != null) {
diff --git a/app/assets/javascripts/project_members.js b/app/assets/javascripts/project_members.js
index f6a796b325a..78f7b48bc7d 100644
--- a/app/assets/javascripts/project_members.js
+++ b/app/assets/javascripts/project_members.js
@@ -5,9 +5,6 @@
return $(this).fadeOut();
});
}
-
return ProjectMembers;
-
})();
-
}).call(this);
diff --git a/app/assets/javascripts/protected_branch_access_dropdown.js.es6 b/app/assets/javascripts/protected_branch_access_dropdown.js.es6
index 2fbb088fa04..7aeb5f92514 100644
--- a/app/assets/javascripts/protected_branch_access_dropdown.js.es6
+++ b/app/assets/javascripts/protected_branch_access_dropdown.js.es6
@@ -10,8 +10,12 @@
selectable: true,
inputId: $dropdown.data('input-id'),
fieldName: $dropdown.data('field-name'),
- toggleLabel(item) {
- return item.text;
+ toggleLabel(item, el) {
+ if (el.is('.is-active')) {
+ return item.text;
+ } else {
+ return 'Select';
+ }
},
clicked(item, $el, e) {
e.preventDefault();
diff --git a/app/assets/javascripts/protected_branch_create.js.es6 b/app/assets/javascripts/protected_branch_create.js.es6
index 2efca2414dc..46beca469b9 100644
--- a/app/assets/javascripts/protected_branch_create.js.es6
+++ b/app/assets/javascripts/protected_branch_create.js.es6
@@ -47,9 +47,7 @@
const $allowedToMergeInput = this.$wrap.find('input[name="protected_branch[merge_access_levels_attributes][0][access_level]"]');
const $allowedToPushInput = this.$wrap.find('input[name="protected_branch[push_access_levels_attributes][0][access_level]"]');
- if ($branchInput.val() && $allowedToMergeInput.val() && $allowedToPushInput.val()){
- this.$form.find('input[type="submit"]').removeAttr('disabled');
- }
+ this.$form.find('input[type="submit"]').attr('disabled', !($branchInput.val() && $allowedToMergeInput.length && $allowedToPushInput.length));
}
}
diff --git a/app/assets/javascripts/protected_branch_edit.js.es6 b/app/assets/javascripts/protected_branch_edit.js.es6
index a59fcbfa082..40bc4adb71b 100644
--- a/app/assets/javascripts/protected_branch_edit.js.es6
+++ b/app/assets/javascripts/protected_branch_edit.js.es6
@@ -31,6 +31,9 @@
const $allowedToMergeInput = this.$wrap.find(`input[name="${this.$allowedToMergeDropdown.data('fieldName')}"]`);
const $allowedToPushInput = this.$wrap.find(`input[name="${this.$allowedToPushDropdown.data('fieldName')}"]`);
+ // Do not update if one dropdown has not selected any option
+ if (!($allowedToMergeInput.length && $allowedToPushInput.length)) return;
+
$.ajax({
type: 'POST',
url: this.$wrap.data('url'),
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js
index 990f6536eb2..227e8c696b4 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js
@@ -7,7 +7,9 @@
KEYCODE = {
ESCAPE: 27,
BACKSPACE: 8,
- ENTER: 13
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
};
function SearchAutocomplete(opts) {
@@ -223,6 +225,12 @@
case KEYCODE.ESCAPE:
this.restoreOriginalState();
break;
+ case KEYCODE.ENTER:
+ this.disableAutocomplete();
+ break;
+ case KEYCODE.UP:
+ case KEYCODE.DOWN:
+ return;
default:
if (this.searchInput.val() === '') {
this.disableAutocomplete();
@@ -319,9 +327,11 @@
};
SearchAutocomplete.prototype.disableAutocomplete = function() {
- this.searchInput.addClass('disabled');
- this.dropdown.removeClass('open');
- return this.restoreMenu();
+ if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
+ this.searchInput.addClass('disabled');
+ this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
+ this.restoreMenu();
+ }
};
SearchAutocomplete.prototype.restoreMenu = function() {
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
new file mode 100644
index 00000000000..855e97eb301
--- /dev/null
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -0,0 +1,12 @@
+/*= require_tree . */
+
+(function() {
+ $(function() {
+ var editor = ace.edit("editor")
+
+ $(".snippet-form-holder form").on('submit', function() {
+ $(".snippet-file-content").val(editor.getValue());
+ });
+ });
+
+}).call(this);
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 8846e08f390..be5c64c56d3 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -90,6 +90,15 @@
width: 100%;
}
}
+
+ // Allows dynamic-width text in the dropdown toggle.
+ // Resizes to allow long text without overflowing the container.
+ &.dynamic {
+ width: auto;
+ min-width: 160px;
+ max-width: 100%;
+ padding-right: 25px;
+ }
}
.dropdown-menu,
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 407f1873431..d3e3fc50736 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -63,9 +63,10 @@
&.image_file {
background: #eee;
text-align: center;
+
img {
- padding: 100px;
- max-width: 50%;
+ padding: 20px;
+ max-width: 80%;
}
}
diff --git a/app/assets/stylesheets/framework/gfm.scss b/app/assets/stylesheets/framework/gfm.scss
index f4d35c4b4b1..c0de09f3968 100644
--- a/app/assets/stylesheets/framework/gfm.scss
+++ b/app/assets/stylesheets/framework/gfm.scss
@@ -2,7 +2,7 @@
* Styles that apply to all GFM related forms.
*/
-.gfm-commit, .gfm-commit_range {
+.gfm-commit_range {
font-family: $monospace_font;
font-size: 90%;
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 26ad2870aa0..8374f30d0b2 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,6 +1,5 @@
.modal-body {
position: relative;
- overflow-y: auto;
padding: 15px;
.form-actions {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 7852fc9a424..9e924f99e9c 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -72,6 +72,7 @@
font-weight: normal;
background-color: #eee;
color: #78a;
+ vertical-align: baseline;
}
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index 21d87cc9d34..b2e22b60440 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -45,7 +45,8 @@
min-width: 175px;
}
-.select2-results .select2-result-label {
+.select2-results .select2-result-label,
+.select2-more-results {
padding: 10px 15px;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 8659604cb8b..06874a993fa 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -14,12 +14,20 @@
margin-top: 0;
}
+ // Single code lines should wrap
code {
font-family: $monospace_font;
- white-space: pre;
+ white-space: pre-wrap;
word-wrap: normal;
}
+ // Multi-line code blocks should scroll horizontally
+ pre {
+ code {
+ white-space: pre;
+ }
+ }
+
kbd {
display: inline-block;
padding: 3px 5px;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index ad4b2d6496f..9ac4d801ac4 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -8,9 +8,13 @@
}
.is-dragging {
+ // Important because plugin sets inline CSS
+ opacity: 1!important;
+
* {
- cursor: -webkit-grabbing;
- cursor: grabbing;
+ // !important to make sure no style can override this when dragging
+ cursor: -webkit-grabbing!important;
+ cursor: grabbing!important;
}
}
@@ -101,8 +105,8 @@
.board {
display: -webkit-flex;
display: flex;
- min-width: calc(100vw - 15px);
- max-width: calc(100vw - 15px);
+ min-width: calc(85vw - 15px);
+ max-width: calc(85vw - 15px);
margin-bottom: 25px;
padding-right: ($gl-padding / 2);
padding-left: ($gl-padding / 2);
@@ -154,14 +158,6 @@
padding: $gl-padding;
font-size: 1em;
border-bottom: 1px solid $border-color;
-
- .board-mobile-handle {
- position: relative;
- left: 0;
- top: 1px;
- margin-top: 0;
- margin-right: 5px;
- }
}
.board-search-container {
@@ -254,11 +250,6 @@
opacity: 0.3;
}
-.is-dragging {
- // Important because plugin sets inline CSS
- opacity: 1!important;
-}
-
.card {
position: relative;
width: 100%;
@@ -269,11 +260,7 @@
list-style: none;
&.user-can-drag {
- padding-left: ($gl-padding * 2);
-
- @media (min-width: $screen-sm-min) {
- padding-left: $gl-padding;
- }
+ padding-left: $gl-padding;
}
&:not(:last-child) {
@@ -294,17 +281,6 @@
}
}
-.board-mobile-handle {
- position: absolute;
- left: 10px;
- top: 50%;
- margin-top: (-15px / 2);
-
- @media (min-width: $screen-sm-min) {
- display: none;
- }
-}
-
.card-title {
margin: 0;
font-size: 1em;
@@ -316,6 +292,7 @@
.card-footer {
margin-top: 5px;
+ line-height: 25px;
.label {
margin-right: 4px;
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 81fce55853c..c1bb250b42d 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -168,7 +168,6 @@
text-overflow: ellipsis;
&:hover {
- background-color: $row-hover;
color: $gl-text-color;
}
}
@@ -190,6 +189,10 @@
display: block;
}
}
+
+ &:hover {
+ background-color: $row-hover;
+ }
}
}
}
diff --git a/app/assets/stylesheets/pages/commit.scss b/app/assets/stylesheets/pages/commit.scss
index bbe0c6c5f1f..53ec0002afe 100644
--- a/app/assets/stylesheets/pages/commit.scss
+++ b/app/assets/stylesheets/pages/commit.scss
@@ -66,6 +66,15 @@
margin-left: 8px;
}
}
+
+ .ci-status-link {
+
+ svg {
+ position: relative;
+ top: 2px;
+ margin: 0 2px 0 3px;
+ }
+ }
}
.ci-status-link {
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 1b389d83525..4d9c73c6840 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -34,11 +34,4 @@
}
}
}
-
- .wiki {
- code {
- white-space: pre-wrap;
- word-break: keep-all;
- }
- }
}
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index abe8414e5e0..fcdcc837298 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -384,3 +384,10 @@
color: $gl-dark-link-color;
}
}
+
+.merge-request-details {
+
+ .title {
+ margin-bottom: 20px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index ce1c424624f..6fa097e3bf1 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -300,6 +300,17 @@
&.playable {
background-color: $gray-light;
+
+ svg {
+ height: 12px;
+ width: 12px;
+ position: relative;
+ top: 1px;
+
+ path {
+ fill: $layout-link-gray;
+ }
+ }
}
.build-content {
@@ -319,10 +330,6 @@
margin-right: 5px;
}
- .fa {
- font-size: 13px;
- }
-
// Connect first build in each stage with right horizontal line
&:first-child {
&::after {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 27dc2b2a1fa..eaf2d3270b3 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -719,3 +719,29 @@ pre.light-well {
width: 300px;
}
}
+
+.clearable-input {
+ position: relative;
+
+ .clear-icon {
+ @extend .fa-times;
+ display: none;
+ position: absolute;
+ right: 7px;
+ top: 7px;
+ color: $location-icon-color;
+
+ &:before {
+ font-family: FontAwesome;
+ font-weight: normal;
+ font-style: normal;
+ }
+ }
+
+ &.has-value {
+ .clear-icon {
+ cursor: pointer;
+ display: block;
+ }
+ }
+}
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index 9e1dc15de84..6ef7cf0bae6 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -109,6 +109,8 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:sentry_dsn,
:akismet_enabled,
:akismet_api_key,
+ :koding_enabled,
+ :koding_url,
:email_author_in_body,
:repository_checks_enabled,
:metrics_packet_size,
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 4ce18321649..cdfa8d91a28 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -42,7 +42,7 @@ class Admin::GroupsController < Admin::ApplicationController
end
def members_update
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(params[:user_ids].split(','), params[:access_level], current_user: current_user)
redirect_to [:admin, @group], notice: 'Users were successfully added.'
end
diff --git a/app/controllers/dashboard/todos_controller.rb b/app/controllers/dashboard/todos_controller.rb
index c8390af3b36..d425d0f9014 100644
--- a/app/controllers/dashboard/todos_controller.rb
+++ b/app/controllers/dashboard/todos_controller.rb
@@ -2,6 +2,7 @@ class Dashboard::TodosController < Dashboard::ApplicationController
before_action :find_todos, only: [:index, :destroy_all]
def index
+ @sort = params[:sort]
@todos = @todos.page(params[:page])
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 9fc41a12536..272164cd0cc 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -21,7 +21,12 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def create
- @group.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @group.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ current_user: current_user,
+ expires_at: params[:expires_at]
+ )
redirect_to group_group_members_path(@group), notice: 'Users were successfully added.'
end
@@ -63,7 +68,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
protected
def member_params
- params.require(:group_member).permit(:access_level, :user_id)
+ params.require(:group_member).permit(:access_level, :user_id, :expires_at)
end
# MembershipActions concern
diff --git a/app/controllers/koding_controller.rb b/app/controllers/koding_controller.rb
new file mode 100644
index 00000000000..bb89f3090f9
--- /dev/null
+++ b/app/controllers/koding_controller.rb
@@ -0,0 +1,15 @@
+class KodingController < ApplicationController
+ before_action :check_integration!, :authenticate_user!, :reject_blocked!
+ layout 'koding'
+
+ def index
+ path = File.join(Rails.root, 'doc/integration/koding-usage.md')
+ @markdown = File.read(path)
+ end
+
+ private
+
+ def check_integration!
+ render_404 unless current_application_settings.koding_enabled?
+ end
+end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 2d894b3dd4a..1a4f6b50e8f 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -12,7 +12,7 @@ module Projects
only: [:iid, :title, :confidential],
include: {
assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority] }
+ labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
})
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 48fe81b0d74..2de8ada3e29 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -15,6 +15,13 @@ class Projects::BranchesController < Projects::ApplicationController
diverging_commit_counts = repository.diverging_commit_counts(branch)
[memo, diverging_commit_counts[:behind], diverging_commit_counts[:ahead]].max
end
+
+ respond_to do |format|
+ format.html
+ format.json do
+ render json: @repository.branch_names
+ end
+ end
end
def recent
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 606552fa853..d0c4550733c 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -11,7 +11,9 @@ class Projects::GroupLinksController < Projects::ApplicationController
return render_404 unless can?(current_user, :read_group, group)
project.project_group_links.create(
- group: group, group_access: params[:link_group_access]
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
)
redirect_to namespace_project_group_links_path(project.namespace, project)
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 3435a118964..42a7e5a2c30 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -36,7 +36,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def create
- @project.team.add_users(params[:user_ids].split(','), params[:access_level], current_user)
+ @project.team.add_users(
+ params[:user_ids].split(','),
+ params[:access_level],
+ expires_at: params[:expires_at],
+ current_user: current_user
+ )
redirect_to namespace_project_project_members_path(@project.namespace, @project)
end
@@ -94,7 +99,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
protected
def member_params
- params.require(:project_member).permit(:user_id, :access_level)
+ params.require(:project_member).permit(:user_id, :access_level, :expires_at)
end
# MembershipActions concern
diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb
index 3334b8556df..79eb45568be 100644
--- a/app/finders/move_to_project_finder.rb
+++ b/app/finders/move_to_project_finder.rb
@@ -1,4 +1,6 @@
class MoveToProjectFinder
+ PAGE_SIZE = 50
+
def initialize(user)
@user = user
end
@@ -8,6 +10,10 @@ class MoveToProjectFinder
projects = projects.search(search) if search.present?
projects = projects.excluding_project(from_project)
+ # infinite scroll using offset
+ projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
+ projects = projects.limit(PAGE_SIZE)
+
# to ask for Project#name_with_namespace
projects.includes(namespace: :owner)
end
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index 37bad596a16..06b3e8a9502 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -33,7 +33,7 @@ class TodosFinder
# the project IDs yielded by the todos query thus far
items = by_project(items)
- items.reorder(id: :desc)
+ sort(items)
end
private
@@ -106,6 +106,10 @@ class TodosFinder
params[:type]
end
+ def sort(items)
+ params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
+ end
+
def by_action(items)
if action?
items = items.where(action: to_action_id)
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 78c0b79d2bd..6de25bea654 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -31,6 +31,10 @@ module ApplicationSettingsHelper
current_application_settings.akismet_enabled?
end
+ def koding_enabled?
+ current_application_settings.koding_enabled?
+ end
+
def allowed_protocols_present?
current_application_settings.enabled_git_access_protocol.present?
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 9ea03720c1e..e13b7cdd707 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -217,4 +217,12 @@ module BlobHelper
def gitlab_ci_ymls
@gitlab_ci_ymls ||= Gitlab::Template::GitlabCiYmlTemplate.dropdown_names
end
+
+ def blob_editor_paths
+ {
+ 'relative-url-root' => Rails.application.config.relative_url_root,
+ 'assets-prefix' => Gitlab::Application.config.assets.prefix,
+ 'blob-language' => @blob && @blob.language.try(:ace_mode)
+ }
+ end
end
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 94df7d131ca..bb285a17baf 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -39,7 +39,7 @@ module CiStatusHelper
when 'running'
'icon_status_running'
when 'play'
- return icon('play fw')
+ 'icon_play'
when 'created'
'icon_status_pending'
else
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 47d174361db..b9baeb1d6c4 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -72,6 +72,15 @@ module IssuablesHelper
end
end
+ def issuable_labels_tooltip(labels, limit: 5)
+ first, last = labels.partition.with_index{ |_, i| i < limit }
+
+ label_names = first.collect(&:name)
+ label_names << "and #{last.size} more" unless last.empty?
+
+ label_names.join(', ')
+ end
+
private
def sidebar_gutter_collapsed?
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 505545fbabb..249d18c4486 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -236,6 +236,60 @@ module ProjectsHelper
)
end
+ def add_koding_stack_path(project)
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch || 'master',
+ file_name: '.koding.yml',
+ commit_message: "Add Koding stack script",
+ content: <<-CONTENT.strip_heredoc
+ provider:
+ aws:
+ access_key: '${var.aws_access_key}'
+ secret_key: '${var.aws_secret_key}'
+ resource:
+ aws_instance:
+ #{project.path}-vm:
+ instance_type: t2.nano
+ user_data: |-
+
+ # Created by GitLab UI for :>
+
+ echo _KD_NOTIFY_@Installing Base packages...@
+
+ apt-get update -y
+ apt-get install git -y
+
+ echo _KD_NOTIFY_@Cloning #{project.name}...@
+
+ export KODING_USER=${var.koding_user_username}
+ export REPO_URL=#{root_url}${var.koding_queryString_repo}.git
+ export BRANCH=${var.koding_queryString_branch}
+
+ sudo -i -u $KODING_USER git clone $REPO_URL -b $BRANCH
+
+ echo _KD_NOTIFY_@#{project.name} cloned.@
+ CONTENT
+ )
+ end
+
+ def koding_project_url(project = nil, branch = nil, sha = nil)
+ if project
+ import_path = "/Home/Stacks/import"
+
+ repo = project.path_with_namespace
+ branch ||= project.default_branch
+ sha ||= project.commit.short_id
+
+ path = "#{import_path}?repo=#{repo}&branch=#{branch}&sha=#{sha}"
+
+ return URI.join(current_application_settings.koding_url, path).to_s
+ end
+
+ current_application_settings.koding_url
+ end
+
def contribution_guide_path(project)
if project && contribution_guide = project.repository.contribution_guide
namespace_project_blob_path(
diff --git a/app/helpers/time_helper.rb b/app/helpers/time_helper.rb
index 790001222f1..271e839692a 100644
--- a/app/helpers/time_helper.rb
+++ b/app/helpers/time_helper.rb
@@ -15,20 +15,9 @@ module TimeHelper
"#{from.to_s(:short)} - #{to.to_s(:short)}"
end
- def duration_in_numbers(finished_at, started_at)
- interval = interval_in_seconds(started_at, finished_at)
- time_format = interval < 1.hour ? "%M:%S" : "%H:%M:%S"
+ def duration_in_numbers(duration)
+ time_format = duration < 1.hour ? "%M:%S" : "%H:%M:%S"
- Time.at(interval).utc.strftime(time_format)
- end
-
- private
-
- def interval_in_seconds(started_at, finished_at = nil)
- if started_at && finished_at
- finished_at.to_i - started_at.to_i
- elsif started_at
- Time.now.to_i - started_at.to_i
- end
+ Time.at(duration).utc.strftime(time_format)
end
end
diff --git a/app/models/ability.rb b/app/models/ability.rb
index 07f703f205d..a49dd703926 100644
--- a/app/models/ability.rb
+++ b/app/models/ability.rb
@@ -166,38 +166,44 @@ class Ability
end
def project_abilities(user, project)
- rules = []
key = "/user/#{user.id}/project/#{project.id}"
- RequestStore.store[key] ||= begin
- # Push abilities on the users team role
- rules.push(*project_team_rules(project.team, user))
+ if RequestStore.active?
+ RequestStore.store[key] ||= uncached_project_abilities(user, project)
+ else
+ uncached_project_abilities(user, project)
+ end
+ end
- owner = user.admin? ||
- project.owner == user ||
- (project.group && project.group.has_owner?(user))
+ def uncached_project_abilities(user, project)
+ rules = []
+ # Push abilities on the users team role
+ rules.push(*project_team_rules(project.team, user))
- if owner
- rules.push(*project_owner_rules)
- end
+ owner = user.admin? ||
+ project.owner == user ||
+ (project.group && project.group.has_owner?(user))
- if project.public? || (project.internal? && !user.external?)
- rules.push(*public_project_rules)
+ if owner
+ rules.push(*project_owner_rules)
+ end
- # Allow to read builds for internal projects
- rules << :read_build if project.public_builds?
+ if project.public? || (project.internal? && !user.external?)
+ rules.push(*public_project_rules)
- unless owner || project.team.member?(user) || project_group_member?(project, user)
- rules << :request_access if project.request_access_enabled
- end
- end
+ # Allow to read builds for internal projects
+ rules << :read_build if project.public_builds?
- if project.archived?
- rules -= project_archived_rules
+ unless owner || project.team.member?(user) || project_group_member?(project, user)
+ rules << :request_access if project.request_access_enabled
end
+ end
- rules - project_disabled_features_rules(project)
+ if project.archived?
+ rules -= project_archived_rules
end
+
+ (rules - project_disabled_features_rules(project)).uniq
end
def project_team_rules(team, user)
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 8c19d9dc9c8..f0bcb2d7cda 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -55,6 +55,10 @@ class ApplicationSetting < ActiveRecord::Base
presence: true,
if: :akismet_enabled
+ validates :koding_url,
+ presence: true,
+ if: :koding_enabled
+
validates :max_attachment_size,
presence: true,
numericality: { only_integer: true, greater_than: 0 }
@@ -149,6 +153,8 @@ class ApplicationSetting < ActiveRecord::Base
two_factor_grace_period: 48,
recaptcha_enabled: false,
akismet_enabled: false,
+ koding_enabled: false,
+ koding_url: nil,
repository_checks_enabled: true,
disabled_oauth_sign_in_sources: [],
send_user_confirmation_email: false,
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index ed056a07a49..096b3b801af 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -62,6 +62,7 @@ module Ci
status_event: 'enqueue'
)
MergeRequests::AddTodoWhenBuildFailsService.new(build.project, nil).close(new_build)
+ build.pipeline.mark_as_processable_after_stage(build.stage_idx)
new_build
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index c360a6ff729..087abe4cbb1 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -78,6 +78,10 @@ module Ci
CommitStatus.where(pipeline: pluck(:id)).stages
end
+ def self.total_duration
+ where.not(duration: nil).sum(:duration)
+ end
+
def stages_with_latest_statuses
statuses.latest.order(:stage_idx).group_by(&:stage)
end
@@ -146,6 +150,10 @@ module Ci
end
end
+ def mark_as_processable_after_stage(stage_idx)
+ builds.skipped.where('stage_idx > ?', stage_idx).find_each(&:process)
+ end
+
def latest?
return false unless ref
commit = project.commit(ref)
@@ -250,7 +258,7 @@ module Ci
end
def update_duration
- self.duration = statuses.latest.duration
+ self.duration = calculate_duration
end
def execute_hooks
diff --git a/app/models/commit.rb b/app/models/commit.rb
index cc413448ce8..817d063e4a2 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -229,7 +229,7 @@ class Commit
def diff_refs
Gitlab::Diff::DiffRefs.new(
- base_sha: self.parent_id || self.sha,
+ base_sha: self.parent_id || Gitlab::Git::BLANK_SHA,
head_sha: self.sha
)
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 703ca90edb6..84ceeac7d3e 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -21,6 +21,7 @@ class CommitStatus < ActiveRecord::Base
where(id: max_id.group(:name, :commit_id))
end
+
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
@@ -30,6 +31,10 @@ class CommitStatus < ActiveRecord::Base
transition [:created, :skipped] => :pending
end
+ event :process do
+ transition skipped: :created
+ end
+
event :run do
transition pending: :running
end
@@ -107,13 +112,7 @@ class CommitStatus < ActiveRecord::Base
end
def duration
- duration =
- if started_at && finished_at
- finished_at - started_at
- elsif started_at
- Time.now - started_at
- end
- duration
+ calculate_duration
end
def stuck?
diff --git a/app/models/concerns/expirable.rb b/app/models/concerns/expirable.rb
new file mode 100644
index 00000000000..be93435453b
--- /dev/null
+++ b/app/models/concerns/expirable.rb
@@ -0,0 +1,15 @@
+module Expirable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :expired, -> { where('expires_at <= ?', Time.current) }
+ end
+
+ def expires?
+ expires_at.present?
+ end
+
+ def expires_soon?
+ expires_at < 7.days.from_now
+ end
+end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index cbae1cd439b..afb5ce37c06 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -131,7 +131,10 @@ module Issuable
end
def order_labels_priority(excluded_labels: [])
- select("#{table_name}.*, (#{highest_label_priority(excluded_labels).to_sql}) AS highest_priority").
+ condition_field = "#{table_name}.id"
+ highest_priority = highest_label_priority(name, condition_field, excluded_labels: excluded_labels).to_sql
+
+ select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
group(arel_table[:id]).
reorder(Gitlab::Database.nulls_last_order('highest_priority', 'ASC'))
end
@@ -159,20 +162,6 @@ module Issuable
grouping_columns
end
-
- private
-
- def highest_label_priority(excluded_labels)
- query = Label.select(Label.arel_table[:priority].minimum).
- joins(:label_links).
- where(label_links: { target_type: name }).
- where("label_links.target_id = #{table_name}.id").
- reorder(nil)
-
- query.where.not(title: excluded_labels) if excluded_labels.present?
-
- query
- end
end
def today?
diff --git a/app/models/concerns/note_on_diff.rb b/app/models/concerns/note_on_diff.rb
index 4be6a2f621b..a881fb83b7f 100644
--- a/app/models/concerns/note_on_diff.rb
+++ b/app/models/concerns/note_on_diff.rb
@@ -17,6 +17,10 @@ module NoteOnDiff
raise NotImplementedError
end
+ def original_line_code
+ raise NotImplementedError
+ end
+
def diff_attributes
raise NotImplementedError
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index 8b47b9e0abd..1ebecd86af9 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -35,5 +35,19 @@ module Sortable
all
end
end
+
+ private
+
+ def highest_label_priority(object_types, condition_field, excluded_labels: [])
+ query = Label.select(Label.arel_table[:priority].minimum).
+ joins(:label_links).
+ where(label_links: { target_type: object_types }).
+ where("label_links.target_id = #{condition_field}").
+ reorder(nil)
+
+ query.where.not(title: excluded_labels) if excluded_labels.present?
+
+ query
+ end
end
end
diff --git a/app/models/concerns/statuseable.rb b/app/models/concerns/statuseable.rb
index 5d4b0a86899..750f937b724 100644
--- a/app/models/concerns/statuseable.rb
+++ b/app/models/concerns/statuseable.rb
@@ -35,11 +35,6 @@ module Statuseable
all.pluck(self.status_sql).first
end
- def duration
- duration_array = all.map(&:duration).compact
- duration_array.reduce(:+)
- end
-
def started_at
all.minimum(:started_at)
end
@@ -85,4 +80,14 @@ module Statuseable
def complete?
COMPLETED_STATUSES.include?(status)
end
+
+ private
+
+ def calculate_duration
+ if started_at && finished_at
+ finished_at - started_at
+ elsif started_at
+ Time.now - started_at
+ end
+ end
end
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 52215f6e2ae..0c23c1c1934 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -16,6 +16,9 @@ class DiffNote < Note
after_initialize :ensure_original_discussion_id
before_validation :set_original_position, :update_position, on: :create
before_validation :set_line_code, :set_original_discussion_id
+ # We need to do this again, because it's already in `Note`, but is affected by
+ # `update_position` and needs to run after that.
+ before_validation :set_discussion_id
after_save :keep_around_commits
class << self
@@ -57,6 +60,10 @@ class DiffNote < Note
diff_file.position(line) == self.original_position
end
+ def original_line_code
+ self.diff_file.line_code(self.diff_line)
+ end
+
def active?(diff_refs = nil)
return false unless supported?
return true if for_commit?
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 3fddc084af2..9676bc03470 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -12,6 +12,7 @@ class Discussion
:for_merge_request?,
:line_code,
+ :original_line_code,
:diff_file,
:for_line?,
:active?,
diff --git a/app/models/group.rb b/app/models/group.rb
index 37631b99701..c48869ae465 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -95,34 +95,40 @@ class Group < Namespace
end
end
- def add_users(user_ids, access_level, current_user = nil)
+ def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
user_ids.each do |user_id|
- Member.add_user(self.group_members, user_id, access_level, current_user)
+ Member.add_user(
+ self.group_members,
+ user_id,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
- def add_user(user, access_level, current_user = nil)
- add_users([user], access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ add_users([user], access_level, current_user: current_user, expires_at: expires_at)
end
def add_guest(user, current_user = nil)
- add_user(user, Gitlab::Access::GUEST, current_user)
+ add_user(user, Gitlab::Access::GUEST, current_user: current_user)
end
def add_reporter(user, current_user = nil)
- add_user(user, Gitlab::Access::REPORTER, current_user)
+ add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
end
def add_developer(user, current_user = nil)
- add_user(user, Gitlab::Access::DEVELOPER, current_user)
+ add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
end
def add_master(user, current_user = nil)
- add_user(user, Gitlab::Access::MASTER, current_user)
+ add_user(user, Gitlab::Access::MASTER, current_user: current_user)
end
def add_owner(user, current_user = nil)
- add_user(user, Gitlab::Access::OWNER, current_user)
+ add_user(user, Gitlab::Access::OWNER, current_user: current_user)
end
def has_owner?(user)
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 8e26cbe9835..40277a9b139 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -49,6 +49,10 @@ class LegacyDiffNote < Note
!line.meta? && diff_file.line_code(line) == self.line_code
end
+ def original_line_code
+ self.line_code
+ end
+
# Check if this note is part of an "active" discussion
#
# This will always return true for anything except MergeRequest noteables,
diff --git a/app/models/member.rb b/app/models/member.rb
index 24ab1276ee9..64e0d33fb20 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -1,6 +1,7 @@
class Member < ActiveRecord::Base
include Sortable
include Importable
+ include Expirable
include Gitlab::Access
attr_accessor :raw_invite_token
@@ -73,7 +74,7 @@ class Member < ActiveRecord::Base
user
end
- def add_user(members, user_id, access_level, current_user = nil)
+ def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
user = user_for_id(user_id)
# `user` can be either a User object or an email to be invited
@@ -87,6 +88,7 @@ class Member < ActiveRecord::Base
if can_update_member?(current_user, member) || project_creator?(member, access_level)
member.created_by ||= current_user
member.access_level = access_level
+ member.expires_at = expires_at
member.save
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 18e97c969d7..ec2d40eb11c 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -34,7 +34,7 @@ class ProjectMember < Member
# :master
# )
#
- def add_users_to_projects(project_ids, user_ids, access, current_user = nil)
+ def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
access_level = if roles_hash.has_key?(access)
roles_hash[access]
elsif roles_hash.values.include?(access.to_i)
@@ -50,7 +50,13 @@ class ProjectMember < Member
project = Project.find(project_id)
users.each do |user|
- Member.add_user(project.project_members, user, access_level, current_user)
+ Member.add_user(
+ project.project_members,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index 3bbf5db0b70..f2656df028b 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -259,6 +259,8 @@ class Note < ActiveRecord::Base
def ensure_discussion_id
return unless self.persisted?
+ # Needed in case the SELECT statement doesn't ask for `discussion_id`
+ return unless self.has_attribute?(:discussion_id)
return if self.discussion_id
set_discussion_id
diff --git a/app/models/project.rb b/app/models/project.rb
index 043da030468..1855760e694 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -611,7 +611,10 @@ class Project < ActiveRecord::Base
end
def new_issue_address(author)
- if Gitlab::IncomingEmail.enabled? && author
+ # This feature is disabled for the time being.
+ return nil
+
+ if Gitlab::IncomingEmail.enabled? && author # rubocop:disable Lint/UnreachableCode
Gitlab::IncomingEmail.reply_address(
"#{path_with_namespace}+#{author.authentication_token}")
end
@@ -1003,8 +1006,8 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
- def add_user(user, access_level, current_user = nil)
- team.add_user(user, access_level, current_user)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
end
def default_branch
diff --git a/app/models/project_group_link.rb b/app/models/project_group_link.rb
index e52a6bd7c84..7613cbdea93 100644
--- a/app/models/project_group_link.rb
+++ b/app/models/project_group_link.rb
@@ -1,4 +1,6 @@
class ProjectGroupLink < ActiveRecord::Base
+ include Expirable
+
GUEST = 10
REPORTER = 20
DEVELOPER = 30
@@ -26,7 +28,7 @@ class ProjectGroupLink < ActiveRecord::Base
self.class.access_options.key(self.group_access)
end
- private
+ private
def different_group
if self.group && self.project && self.project.group == self.group
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d0a714cd6fc..ab6ea2aae36 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -15,9 +15,9 @@ class ProjectTeam
users, access, current_user = *args
if users.respond_to?(:each)
- add_users(users, access, current_user)
+ add_users(users, access, current_user: current_user)
else
- add_user(users, access, current_user)
+ add_user(users, access, current_user: current_user)
end
end
@@ -33,17 +33,18 @@ class ProjectTeam
member
end
- def add_users(users, access, current_user = nil)
+ def add_users(users, access, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects(
[project.id],
users,
access,
- current_user
+ current_user: current_user,
+ expires_at: expires_at
)
end
- def add_user(user, access, current_user = nil)
- add_users([user], access, current_user)
+ def add_user(user, access, current_user: nil, expires_at: nil)
+ add_users([user], access, current_user: current_user, expires_at: expires_at)
end
# Remove all users from project team
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 2494c266cd2..bdc3b9d1c1c 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -277,7 +277,7 @@ class Repository
def cache_keys
%i(size commit_count
readme version contribution_guide changelog
- license_blob license_key gitignore)
+ license_blob license_key gitignore koding_yml)
end
# Keys for data on branch/tag operations.
@@ -553,6 +553,14 @@ class Repository
end
end
+ def koding_yml
+ return nil unless head_exists?
+
+ cache.fetch(:koding_yml) do
+ file_on_head(/\A\.koding\.yml\z/)
+ end
+ end
+
def gitlab_ci_yml
return nil unless head_exists?
diff --git a/app/models/todo.rb b/app/models/todo.rb
index 8d7a5965aa1..6ae9956ade5 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -1,4 +1,6 @@
class Todo < ActiveRecord::Base
+ include Sortable
+
ASSIGNED = 1
MENTIONED = 2
BUILD_FAILED = 3
@@ -41,6 +43,23 @@ class Todo < ActiveRecord::Base
after_save :keep_around_commit
+ class << self
+ def sort(method)
+ method == "priority" ? order_by_labels_priority : order_by(method)
+ end
+
+ # Order by priority depending on which issue/merge request the Todo belongs to
+ # Todos with highest priority first then oldest todos
+ # Need to order by created_at last because of differences on Mysql and Postgres when joining by type "Merge_request/Issue"
+ def order_by_labels_priority
+ highest_priority = highest_label_priority(["Issue", "MergeRequest"], "todos.target_id").to_sql
+
+ select("#{table_name}.*, (#{highest_priority}) AS highest_priority").
+ order(Gitlab::Database.nulls_last_order('highest_priority', 'ASC')).
+ order('todos.created_at')
+ end
+ end
+
def build_failed?
action == BUILD_FAILED
end
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
new file mode 100644
index 00000000000..ca9db59cac7
--- /dev/null
+++ b/app/services/members/authorized_destroy_service.rb
@@ -0,0 +1,19 @@
+module Members
+ class AuthorizedDestroyService < BaseService
+ attr_accessor :member, :user
+
+ def initialize(member, user = nil)
+ @member, @user = member, user
+ end
+
+ def execute
+ return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
+
+ member.destroy
+
+ if member.request? && member.user != user
+ notification_service.decline_access_request(member)
+ end
+ end
+ end
+end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 9e3f6af628d..9a2bf82ef51 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -11,12 +11,7 @@ module Members
unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
raise Gitlab::Access::AccessDeniedError
end
-
- member.destroy
-
- if member.request? && member.user != current_user
- notification_service.decline_access_request(member)
- end
+ AuthorizedDestroyService.new(member, current_user).execute
end
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 66a838b3d13..6139ed56e25 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -242,7 +242,6 @@ class NotificationService
project_member.real_source_type,
project_member.project.id,
project_member.invite_email,
- project_member.access_level,
project_member.created_by_id
).deliver_later
end
@@ -269,7 +268,6 @@ class NotificationService
group_member.real_source_type,
group_member.group.id,
group_member.invite_email,
- group_member.access_level,
group_member.created_by_id
).deliver_later
end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index c7fd344eea2..e0878512e62 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -388,6 +388,25 @@
.help-block
If you got a lot of false alarms from repository checks you can choose to clear all repository check information from the database.
+ %fieldset
+ %legend Koding
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :koding_enabled do
+ = f.check_box :koding_enabled
+ Enable Koding
+ .form-group
+ = f.label :koding_url, 'Koding URL', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_field :koding_url, class: 'form-control', placeholder: 'http://gitlab.your-koding-instance.com:8090'
+ .help-block
+ Koding has integration enabled out of the box for the
+ %strong gitlab
+ team, and you need to provide that team's URL here. Learn more in the
+ = succeed "." do
+ = link_to "Koding integration documentation", help_page_path("integration/koding")
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/builds/_build.html.haml b/app/views/admin/builds/_build.html.haml
index 352adbedee4..f29d9c94441 100644
--- a/app/views/admin/builds/_build.html.haml
+++ b/app/views/admin/builds/_build.html.haml
@@ -51,7 +51,7 @@
- if build.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(build.finished_at, build.started_at)
+ = duration_in_numbers(build.duration)
- if build.finished_at
%p.finished-at
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 4e340b6ec16..d320d3bcc1e 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -43,6 +43,25 @@
class: 'select2 trigger-submit', include_blank: true,
data: {placeholder: 'Action'})
+ .pull-right
+ .dropdown.inline.prepend-left-10
+ %button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
+ %span.light
+ - if @sort.present?
+ = sort_options_hash[@sort]
+ - else
+ = sort_title_recently_created
+ %b.caret
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
+ %li
+ = link_to todos_filter_path(sort: sort_value_priority) do
+ = sort_title_priority
+ = link_to todos_filter_path(sort: sort_value_recently_created) do
+ = sort_title_recently_created
+ = link_to todos_filter_path(sort: sort_value_oldest_created) do
+ = sort_title_oldest_created
+
+
.prepend-top-default
- if @todos.any?
.js-todos-options{ data: {per_page: @todos.limit_value, current_page: @todos.current_page, total_pages: @todos.total_pages} }
diff --git a/app/views/discussions/_diff_with_notes.html.haml b/app/views/discussions/_diff_with_notes.html.haml
index b2e55f7647a..3a95a652810 100644
--- a/app/views/discussions/_diff_with_notes.html.haml
+++ b/app/views/discussions/_diff_with_notes.html.haml
@@ -7,7 +7,7 @@
.diff-content.code.js-syntax-highlight
%table
- - discussions = { discussion.line_code => discussion }
+ - discussions = { discussion.original_line_code => discussion }
= render partial: "projects/diffs/line",
collection: discussion.truncated_diff_lines,
as: :line,
diff --git a/app/views/groups/group_members/_new_group_member.html.haml b/app/views/groups/group_members/_new_group_member.html.haml
index 9bb9f962177..2fb3190ab11 100644
--- a/app/views/groups/group_members/_new_group_member.html.haml
+++ b/app/views/groups/group_members/_new_group_member.html.haml
@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this group and all of its projects.
+
.form-actions
= f.submit 'Add users to group', class: "btn btn-create"
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
index da71de4cd1e..742f9d7a433 100644
--- a/app/views/groups/group_members/update.js.haml
+++ b/app/views/groups/group_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@group_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @group_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/koding/index.html.haml b/app/views/koding/index.html.haml
new file mode 100644
index 00000000000..111cc67336c
--- /dev/null
+++ b/app/views/koding/index.html.haml
@@ -0,0 +1,9 @@
+.row-content-block.second-block.center
+ %p
+ = icon('circle', class: 'cgreen')
+ Integration is active for
+ = link_to koding_project_url, target: '_blank' do
+ #{current_application_settings.koding_url}
+
+.documentation.wiki
+ = markdown @markdown
diff --git a/app/views/layouts/koding.html.haml b/app/views/layouts/koding.html.haml
new file mode 100644
index 00000000000..22319bba745
--- /dev/null
+++ b/app/views/layouts/koding.html.haml
@@ -0,0 +1,5 @@
+- page_title "Koding"
+- page_description "Koding Dashboard"
+- header_title "Koding", koding_path
+
+= render template: "layouts/application"
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 3a14751ea8e..67f558c854b 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -12,6 +12,11 @@
= link_to activity_dashboard_path, class: 'dashboard-shortcuts-activity', title: 'Activity' do
%span
Activity
+ - if koding_enabled?
+ = nav_link(controller: :koding) do
+ = link_to koding_path, title: 'Koding' do
+ %span
+ Koding
= nav_link(controller: [:groups, 'groups/milestones', 'groups/group_members']) do
= link_to dashboard_groups_path, title: 'Groups' do
%span
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 1d3b8fc3683..f7012595a5a 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -65,7 +65,7 @@
Graphs
- if project_nav_tab? :issues
- = nav_link(controller: [:issues, :labels, :milestones]) do
+ = nav_link(controller: [:issues, :labels, :milestones, :boards]) do
= link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues', class: 'shortcuts-issues' do
%span
Issues
diff --git a/app/views/projects/blob/edit.html.haml b/app/views/projects/blob/edit.html.haml
index 7b0621f9401..680e95ac6b5 100644
--- a/app/views/projects/blob/edit.html.haml
+++ b/app/views/projects/blob/edit.html.haml
@@ -1,4 +1,7 @@
- page_title "Edit", @blob.path, @ref
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
- if @conflict
.alert.alert-danger
@@ -16,14 +19,10 @@
= link_to '#preview', 'data-preview-url' => namespace_project_preview_blob_path(@project.namespace, @project, @id) do
= editing_preview_title(@blob.name)
- = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form') do
+ = form_tag(namespace_project_update_blob_path(@project.namespace, @project, @id), method: :put, class: 'form-horizontal js-quick-submit js-requires-input js-edit-blob-form', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref, path: @path, blob_data: @blob.data
= render 'shared/new_commit_form', placeholder: "Update #{@blob.name}"
= hidden_field_tag 'last_commit_sha', @last_commit_sha
= hidden_field_tag 'content', '', id: "file-content"
= hidden_field_tag 'from_merge_request_id', params[:from_merge_request_id]
= render 'projects/commit_button', ref: @ref, cancel_path: namespace_project_blob_path(@project.namespace, @project, @id)
-
-:javascript
- blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", "#{@blob.language.try(:ace_mode)}")
- new NewCommitForm($('.js-edit-blob-form'))
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index c952bc7e5db..b6ed9518c48 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -1,17 +1,16 @@
- page_title "New File", @path.presence, @ref
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('blob_edit/blob_edit_bundle.js')
%h3.page-title
New File
.file-editor
- = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-new-blob-form js-quick-submit js-requires-input') do
+ = form_tag(namespace_project_create_blob_path(@project.namespace, @project, @id), method: :post, class: 'form-horizontal js-edit-blob-form js-new-blob-form js-quick-submit js-requires-input', data: blob_editor_paths) do
= render 'projects/blob/editor', ref: @ref
= render 'shared/new_commit_form', placeholder: "Add new file"
= hidden_field_tag 'content', '', id: 'file-content'
= render 'projects/commit_button', ref: @ref,
cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
-
-:javascript
- blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}")
- new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index f8ebf397ee2..de53a298f84 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -11,7 +11,6 @@
.board-inner
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
- = icon("align-justify", class: "board-mobile-handle js-board-drag-handle", "v-if" => "(!disabled && !list.preset)")
{{ list.title }}
%span.pull-right{ "v-if" => "list.type !== 'blank'" }
{{ list.issues.length }}
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index b20c23f6b8e..e8b60b54d80 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -9,7 +9,6 @@
"track-by" => "id" }
%li.card{ ":class" => "{ 'user-can-drag': !disabled }",
":index" => "index" }
- = icon("align-justify", class: "board-mobile-handle js-card-drag-handle", "v-if" => "!disabled")
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
%a{ ":href" => "issueLinkBase + '/' + issue.id",
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 4bd85061240..6192ccb710b 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -5,8 +5,8 @@
- number_commits_ahead = diverging_commit_counts[:ahead]
%li(class="js-branch-#{branch.name}")
%div
- = link_to namespace_project_tree_path(@project.namespace, @project, branch.name) do
- %span.item-title.str-truncated= branch.name
+ = link_to namespace_project_tree_path(@project.namespace, @project, branch.name), class: 'item-title str-truncated' do
+ = branch.name
&nbsp;
- if branch.name == @repository.root_ref
%span.label.label-primary default
diff --git a/app/views/projects/buttons/_koding.html.haml b/app/views/projects/buttons/_koding.html.haml
new file mode 100644
index 00000000000..fdc80d44253
--- /dev/null
+++ b/app/views/projects/buttons/_koding.html.haml
@@ -0,0 +1,7 @@
+- if koding_enabled? && current_user && can_push_branch?(@project, @project.default_branch)
+ - if @repository.koding_yml
+ = link_to koding_project_url(@project), class: 'btn', target: '_blank' do
+ Run in IDE (Koding)
+ - else
+ = link_to add_koding_stack_path(@project), class: 'btn' do
+ Set Up Koding
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 91081435220..1fdf32466f2 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -63,7 +63,7 @@
- if build.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(build.finished_at, build.started_at)
+ = duration_in_numbers(build.duration)
- if build.finished_at
%p.finished-at
= icon("calendar")
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index be387201f8d..9a672b23341 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -48,10 +48,10 @@
\-
%td
- - if pipeline.started_at && pipeline.finished_at
+ - if pipeline.duration
%p.duration
= custom_icon("icon_timer")
- = duration_in_numbers(pipeline.finished_at, pipeline.started_at)
+ = duration_in_numbers(pipeline.duration)
- if pipeline.finished_at
%p.finished-at
= icon("calendar")
diff --git a/app/views/projects/commit/_change.html.haml b/app/views/projects/commit/_change.html.haml
index d9b800a4ded..e4cd55b9f7a 100644
--- a/app/views/projects/commit/_change.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -17,7 +17,9 @@
.form-group.branch
= label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
- = select_tag "target_branch", project_branches, class: "select2 select2-sm js-target-branch"
+ = hidden_field_tag :target_branch, @project.default_branch, id: 'target_branch'
+ = dropdown_tag(@project.default_branch, options: { title: "Switch branch", filter: true, placeholder: "Search branches", toggle_class: 'js-project-refs-dropdown js-target-branch dynamic', dropdown_class: 'dropdown-menu-selectable', data: { field_name: "target_branch", selected: @project.default_branch, target_branch: @project.default_branch, refs_url: namespace_project_branches_path(@project.namespace, @project), submit_form_on_click: false }})
+
- if can?(current_user, :push_code, @project)
.js-create-merge-request-container
.checkbox
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 3ad866bb2f1..29d767e7769 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -56,10 +56,10 @@
= 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)
- = ci_label_for_status(@commit.status)
- - if @commit.pipelines.duration
- in
- = time_interval_in_words @commit.pipelines.duration
+ %span.ci-status-label
+ = ci_label_for_status(@commit.status)
+ in
+ = time_interval_in_words @commit.pipelines.total_duration
.commit-box.content-block
%h3.commit-title
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index 2b904544f28..ca700cb3a3b 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -17,6 +17,13 @@
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
%span.caret
+ .form-group
+ = label_tag :expires_at, 'Access expiration date', class: 'label-light'
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, all users in the group will automatically lose access to this project.
= submit_tag "Share", class: "btn btn-create"
.col-lg-9.col-lg-offset-3
%hr
@@ -35,6 +42,10 @@
= group.name
%br
up to #{group_link.human_access}
+ - if group_link.expires?
+ ·
+ %span{ class: ('text-warning' if group_link.expires_soon?) }
+ expires in #{distance_of_time_in_words_to_now(group_link.expires_at)}
.pull-right
= link_to namespace_project_group_link_path(@project.namespace, @project, group_link), method: :delete, class: "btn btn-transparent" do
%span.sr-only disable sharing
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 9f1a046ea74..3fb4191c60e 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -22,7 +22,7 @@
- if can?(current_user, :create_issue, @project) || can?(current_user, :update_issue, @issue)
.issuable-actions
.clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index f8025fc1dbe..9d8b4cc56be 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -16,6 +16,9 @@
- if @merge_request.open?
.pull-right
- if @merge_request.source_branch_exists?
+ - if koding_enabled? && @repository.koding_yml
+ = link_to koding_project_url(@merge_request.source_project, @merge_request.source_branch, @merge_request.commits.first.short_id), class: "btn inline btn-grouped btn-sm", target: '_blank' do
+ Run in IDE (Koding)
= link_to "#modal_merge_info", class: "btn inline btn-grouped btn-sm", "data-toggle" => "modal" do
Check out branch
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index b24bdf22ceb..098ce19da21 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -14,7 +14,7 @@
- if can?(current_user, :update_merge_request, @merge_request)
.issuable-actions
.clearfix.issue-btn-group.dropdown
- %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ data: { toggle: "dropdown" } }
+ %button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
%span.caret
Options
.dropdown-menu.dropdown-menu-align-right.hidden-lg
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 8289aefcde7..063e83a407a 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -9,7 +9,7 @@
= link_to @pipeline.ref, namespace_project_commits_path(@project.namespace, @project, @pipeline.ref), class: "monospace"
- if @pipeline.duration
in
- = time_interval_in_words @pipeline.duration
+ = time_interval_in_words(@pipeline.duration)
.pull-right
= link_to namespace_project_pipeline_path(@project.namespace, @project, @pipeline), class: "ci-status ci-#{@pipeline.status}" do
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 978c4dfc5ec..fa8cbf71733 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -14,5 +14,14 @@
Read more about role permissions
%strong= link_to "here", help_page_path("user/permissions"), class: "vlink"
+ .form-group
+ = f.label :expires_at, 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date'
+ %i.clear-icon.js-clear-input
+ .help-block
+ On this date, the user(s) will automatically lose access to this project.
+
.form-actions
= f.submit 'Add users to project', class: "btn btn-create"
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 9031f01b496..9d063b3081f 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -1,6 +1,6 @@
- page_title "Members"
-.project-members-page.prepend-top-default
+.project-members-page.js-project-members-page.prepend-top-default
- if can?(current_user, :admin_project_member, @project)
.panel.panel-default
.panel-heading
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
index 45f8ef89060..833954bc039 100644
--- a/app/views/projects/project_members/update.js.haml
+++ b/app/views/projects/project_members/update.js.haml
@@ -1,2 +1,3 @@
:plain
$("##{dom_id(@project_member)}").replaceWith('#{escape_javascript(render('shared/members/member', member: @project_member))}');
+ new MemberExpirationDate();
diff --git a/app/views/projects/protected_branches/_create_protected_branch.html.haml b/app/views/projects/protected_branches/_create_protected_branch.html.haml
index d4c6fa24768..e95a3b1b4c3 100644
--- a/app/views/projects/protected_branches/_create_protected_branch.html.haml
+++ b/app/views/projects/protected_branches/_create_protected_branch.html.haml
@@ -22,16 +22,20 @@
%label.col-md-2.text-right{ for: 'merge_access_levels_attributes' }
Allowed to merge:
.col-md-10
- = dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-merge wide',
- data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
+ .merge_access_levels-container
+ = dropdown_tag('Select',
+ options: { toggle_class: 'js-allowed-to-merge wide',
+ dropdown_class: 'dropdown-menu-selectable',
+ data: { field_name: 'protected_branch[merge_access_levels_attributes][0][access_level]', input_id: 'merge_access_levels_attributes' }})
.form-group
%label.col-md-2.text-right{ for: 'push_access_levels_attributes' }
Allowed to push:
.col-md-10
- = dropdown_tag('Select',
- options: { toggle_class: 'js-allowed-to-push wide',
- data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
+ .push_access_levels-container
+ = dropdown_tag('Select',
+ options: { toggle_class: 'js-allowed-to-push wide',
+ dropdown_class: 'dropdown-menu-selectable',
+ data: { field_name: 'protected_branch[push_access_levels_attributes][0][access_level]', input_id: 'push_access_levels_attributes' }})
.panel-footer
= f.submit 'Protect', class: 'btn-create btn', disabled: true
diff --git a/app/views/projects/releases/edit.html.haml b/app/views/projects/releases/edit.html.haml
index 835398b6f98..33d5cbff420 100644
--- a/app/views/projects/releases/edit.html.haml
+++ b/app/views/projects/releases/edit.html.haml
@@ -1,18 +1,20 @@
+- @no_container = true
- page_title "Edit", @tag.name, "Tags"
= render "projects/commits/head"
-.row-content-block
- .oneline
- .title
- Release notes for tag
- %strong #{@tag.name}
+%div{ class: container_class }
+ .sub-header-block.no-bottom-space
+ .oneline
+ .title
+ Release notes for tag
+ %strong #{@tag.name}
+
-.prepend-top-default
= form_for(@release, method: :put, url: namespace_project_tag_release_path(@project.namespace, @project, @tag.name), html: { class: 'form-horizontal common-note-form release-form js-quick-submit' }) do |f|
= render layout: 'projects/md_preview', locals: { preview_class: "md-preview", referenced_users: true } do
= render 'projects/zen', f: f, attr: :description, classes: 'note-textarea', placeholder: "Write your release notes or drag files here..."
= render 'projects/notes/hints'
.error-alert
- .form-actions.prepend-top-default
+ .prepend-top-default
= f.submit 'Save changes', class: 'btn btn-save'
= link_to "Cancel", namespace_project_tag_path(@project.namespace, @project, @tag.name), class: "btn btn-default btn-cancel"
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index a666d07e9eb..340e159c874 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -64,10 +64,12 @@
%li.missing
= link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
Set Up CI
+
%li.project-repo-buttons-right
.project-repo-buttons.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
+ = render "projects/buttons/koding"
.btn-group.project-repo-btn-group
= render "projects/buttons/download"
@@ -86,4 +88,4 @@
Archived project! Repository is read-only
%div{class: "project-show-#{default_project_view}"}
- = render default_project_view \ No newline at end of file
+ = render default_project_view
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index ea7162d4d63..9a8252ab087 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -6,7 +6,7 @@
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
.dropdown
- = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" }
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project), field_name: 'ref', submit_form_on_click: true }, { toggle_class: "js-project-refs-dropdown" }
.dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
= dropdown_title "Switch branch/tag"
= dropdown_filter "Search branches and tags"
diff --git a/app/views/shared/icons/_icon_play.svg b/app/views/shared/icons/_icon_play.svg
new file mode 100644
index 00000000000..80a6d41dbf6
--- /dev/null
+++ b/app/views/shared/icons/_icon_play.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 10 11"><path fill-rule="evenodd" d="m9.283 6.47l-7.564 4.254c-.949.534-1.719.266-1.719-.576v-9.292c0-.852.756-1.117 1.719-.576l7.564 4.254c.949.534.963 1.392 0 1.934"/></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index d717c3d92ee..544ed6203aa 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -121,7 +121,7 @@
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
.issuable-form-select-holder
- = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id) }
+ = hidden_field_tag :move_to_project_id, nil, class: 'js-move-dropdown', data: { placeholder: 'Select project', projects_url: autocomplete_projects_path(project_id: @project.id), page_size: MoveToProjectFinder::PAGE_SIZE }
&nbsp;
%span{ data: { toggle: 'tooltip', placement: 'auto top' }, style: 'cursor: default',
title: 'Moving an issue will copy the discussion to a different project and close it here. All participants will be notified of the new location.' }
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 8e2fcbdfab8..c1b50e65af5 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -109,7 +109,7 @@
- if issuable.project.labels.any?
.block.labels
- .sidebar-collapsed-icon
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
= icon('tags')
%span
= issuable.labels_array.size
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index fc6e206d082..5f20e4bd42a 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -16,7 +16,7 @@
= button_tag icon('pencil'),
type: 'button',
class: 'btn inline js-toggle-button',
- title: 'Edit access level'
+ title: 'Edit'
- if member.request?
= link_to icon('check inverse'), polymorphic_path([:approve_access_request, member]),
@@ -59,6 +59,10 @@
= time_ago_with_tooltip(member.requested_at)
- else
Joined #{time_ago_with_tooltip(member.created_at)}
+ - if member.expires?
+ ·
+ %span{ class: ('text-warning' if member.expires_soon?) }
+ Expires in #{distance_of_time_in_words_to_now(member.expires_at)}
- else
= image_tag avatar_icon(member.invite_email, 40), class: "avatar s40", alt: ''
@@ -73,8 +77,16 @@
- if show_roles
.edit-member.hide.js-toggle-content
%br
- = form_for member, remote: true do |f|
- .prepend-top-10
- = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control'
+ = form_for member, remote: true, html: { class: 'form-horizontal' } do |f|
+ .form-group
+ = label_tag "member_access_level_#{member.id}", 'Project access', class: 'control-label'
+ .col-sm-10
+ = f.select :access_level, options_for_select(member.class.access_level_roles, member.access_level), {}, class: 'form-control', id: "member_access_level_#{member.id}"
+ .form-group
+ = label_tag "member_expires_at_#{member.id}", 'Access expiration date', class: 'control-label'
+ .col-sm-10
+ .clearable-input
+ = f.text_field :expires_at, class: 'form-control js-access-expiration-date', placeholder: 'Select access expiration date', id: "member_expires_at_#{member.id}"
+ %i.clear-icon.js-clear-input
.prepend-top-10
= f.submit 'Save', class: 'btn btn-save btn-sm'
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 47ec09f62c6..0c788032020 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,3 +1,7 @@
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/ace.js')
+ = page_specific_javascript_tag('snippet/snippet_bundle.js')
+
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input" } do |f|
= form_errors(@snippet)
@@ -31,8 +35,3 @@
- else
= link_to "Cancel", snippets_path(@project), class: "btn btn-cancel"
-:javascript
- var editor = ace.edit("editor");
- $(".snippet-form-holder form").submit(function(){
- $(".snippet-file-content").val(editor.getValue());
- });
diff --git a/app/workers/emails_on_push_worker.rb b/app/workers/emails_on_push_worker.rb
index c6a5af2809a..1dc7e0adef7 100644
--- a/app/workers/emails_on_push_worker.rb
+++ b/app/workers/emails_on_push_worker.rb
@@ -33,13 +33,13 @@ class EmailsOnPushWorker
reverse_compare = false
if action == :push
- compare = CompareService.new.execute(project, before_sha, project, after_sha)
+ compare = CompareService.new.execute(project, after_sha, project, before_sha)
diff_refs = compare.diff_refs
return false if compare.same
if compare.commits.empty?
- compare = CompareService.new.execute(project, after_sha, project, before_sha)
+ compare = CompareService.new.execute(project, before_sha, project, after_sha)
diff_refs = compare.diff_refs
reverse_compare = true
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
new file mode 100644
index 00000000000..246c8b6650a
--- /dev/null
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -0,0 +1,7 @@
+class RemoveExpiredGroupLinksWorker
+ include Sidekiq::Worker
+
+ def perform
+ ProjectGroupLink.expired.destroy_all
+ end
+end
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
new file mode 100644
index 00000000000..cf765af97ce
--- /dev/null
+++ b/app/workers/remove_expired_members_worker.rb
@@ -0,0 +1,13 @@
+class RemoveExpiredMembersWorker
+ include Sidekiq::Worker
+
+ def perform
+ Member.expired.find_each do |member|
+ begin
+ Members::AuthorizedDestroyService.new(member).execute
+ rescue => ex
+ logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
+ end
+ end
+ end
+end
diff --git a/config/application.rb b/config/application.rb
index 6b80f8ddafa..4792f6670a8 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -88,6 +88,8 @@ module Gitlab
config.assets.precompile << "diff_notes/diff_notes_bundle.js"
config.assets.precompile << "boards/boards_bundle.js"
config.assets.precompile << "boards/test_utils/simulate_drag.js"
+ config.assets.precompile << "blob_edit/blob_edit_bundle.js"
+ config.assets.precompile << "snippet/snippet_bundle.js"
config.assets.precompile << "lib/utils/*.js"
config.assets.precompile << "lib/*.js"
config.assets.precompile << "u2f.js"
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index deac3b0f0f9..7a9376def02 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -293,6 +293,12 @@ Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'Impor
Settings.cron_jobs['requests_profiles_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['requests_profiles_worker']['cron'] ||= '0 0 * * *'
Settings.cron_jobs['requests_profiles_worker']['job_class'] = 'RequestsProfilesWorker'
+Settings.cron_jobs['remove_expired_members_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_members_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_members_worker']['job_class'] = 'RemoveExpiredMembersWorker'
+Settings.cron_jobs['remove_expired_group_links_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['remove_expired_group_links_worker']['cron'] ||= '10 0 * * *'
+Settings.cron_jobs['remove_expired_group_links_worker']['job_class'] = 'RemoveExpiredGroupLinksWorker'
#
# GitLab Shell
diff --git a/config/routes.rb b/config/routes.rb
index 66f77aee06a..e93b640fbc0 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -91,6 +91,11 @@ Rails.application.routes.draw do
get 'help/*path' => 'help#show', as: :help_page
#
+ # Koding route
+ #
+ get 'koding' => 'koding#index'
+
+ #
# Global snippets
#
resources :snippets do
diff --git a/db/fixtures/development/14_builds.rb b/db/fixtures/development/14_pipelines.rb
index 069d9dd6226..49e6e2361b1 100644
--- a/db/fixtures/development/14_builds.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -1,4 +1,4 @@
-class Gitlab::Seeder::Builds
+class Gitlab::Seeder::Pipelines
STAGES = %w[build test deploy notify]
BUILDS = [
{ name: 'build:linux', stage: 'build', status: :success },
@@ -7,11 +7,12 @@ class Gitlab::Seeder::Builds
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:windows', stage: 'test', status: :success },
{ name: 'rspec:osx', stage: 'test', status_event: :success },
- { name: 'spinach:linux', stage: 'test', status: :pending },
- { name: 'spinach:osx', stage: 'test', status: :canceled },
- { name: 'cucumber:linux', stage: 'test', status: :running },
- { name: 'cucumber:osx', stage: 'test', status: :failed },
- { name: 'staging', stage: 'deploy', environment: 'staging', status: :success },
+ { name: 'spinach:linux', stage: 'test', status: :success },
+ { name: 'spinach:osx', stage: 'test', status: :failed, allow_failure: true},
+ { name: 'env:alpha', stage: 'deploy', environment: 'alpha', status: :pending },
+ { name: 'env:beta', stage: 'deploy', environment: 'beta', status: :running },
+ { name: 'env:gamma', stage: 'deploy', environment: 'gamma', status: :canceled },
+ { name: 'staging', stage: 'deploy', environment: 'staging', status_event: :success },
{ name: 'production', stage: 'deploy', environment: 'production', when: 'manual', status: :skipped },
{ name: 'slack', stage: 'notify', when: 'manual', status: :created },
]
@@ -34,72 +35,86 @@ class Gitlab::Seeder::Builds
end
end
+ private
+
def pipelines
- master_pipelines + merge_request_pipelines
+ create_master_pipelines + create_merge_request_pipelines
end
- def master_pipelines
- create_pipelines_for(@project, 'master')
+ def create_master_pipelines
+ @project.repository.commits('master', limit: 4).map do |commit|
+ create_pipeline!(@project, 'master', commit)
+ end
rescue
[]
end
- def merge_request_pipelines
- @project.merge_requests.last(5).map do |merge_request|
- create_pipelines(merge_request.source_project, merge_request.source_branch, merge_request.commits.last(5))
- end.flatten
+ def create_merge_request_pipelines
+ pipelines = @project.merge_requests.first(3).map do |merge_request|
+ project = merge_request.source_project
+ branch = merge_request.source_branch
+
+ merge_request.commits.last(4).map do |commit|
+ create_pipeline!(project, branch, commit)
+ end
+ end
+
+ pipelines.flatten
rescue
[]
end
- def create_pipelines_for(project, ref)
- commits = project.repository.commits(ref, limit: 5)
- create_pipelines(project, ref, commits)
+
+ def create_pipeline!(project, ref, commit)
+ project.pipelines.create(sha: commit.id, ref: ref)
end
- def create_pipelines(project, ref, commits)
- commits.map do |commit|
- project.pipelines.create(sha: commit.id, ref: ref)
+ def build_create!(pipeline, opts = {})
+ attributes = job_attributes(pipeline, opts)
+ .merge(commands: '$ build command')
+
+ Ci::Build.create!(attributes).tap do |build|
+ # We need to set build trace and artifacts after saving a build
+ # (id required), that is why we need `#tap` method instead of passing
+ # block directly to `Ci::Build#create!`.
+
+ setup_artifacts(build)
+ setup_build_log(build)
+ build.save
end
end
- def build_create!(pipeline, opts = {})
- attributes = build_attributes_for(pipeline, opts)
+ def setup_artifacts(build)
+ return unless %w[build test].include?(build.stage)
- Ci::Build.create!(attributes) do |build|
- if opts[:name].start_with?('build')
- artifacts_cache_file(artifacts_archive_path) do |file|
- build.artifacts_file = file
- end
+ artifacts_cache_file(artifacts_archive_path) do |file|
+ build.artifacts_file = file
+ end
- artifacts_cache_file(artifacts_metadata_path) do |file|
- build.artifacts_metadata = file
- end
- end
+ artifacts_cache_file(artifacts_metadata_path) do |file|
+ build.artifacts_metadata = file
+ end
+ end
- if %w(running success failed).include?(build.status)
- # We need to set build trace after saving a build (id required)
- build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
- end
+ def setup_build_log(build)
+ if %w(running success failed).include?(build.status)
+ build.trace = FFaker::Lorem.paragraphs(6).join("\n\n")
end
end
def commit_status_create!(pipeline, opts = {})
- attributes = commit_status_attributes_for(pipeline, opts)
+ attributes = job_attributes(pipeline, opts)
+
GenericCommitStatus.create!(attributes)
end
- def commit_status_attributes_for(pipeline, opts)
+ def job_attributes(pipeline, opts)
{ name: 'test build', stage: 'test', stage_idx: stage_index(opts[:stage]),
ref: 'master', tag: false, user: build_user, project: @project, pipeline: pipeline,
created_at: Time.now, updated_at: Time.now
}.merge(opts)
end
- def build_attributes_for(pipeline, opts)
- commit_status_attributes_for(pipeline, opts).merge(commands: '$ build command')
- end
-
def build_user
@project.team.users.sample
end
@@ -131,8 +146,8 @@ class Gitlab::Seeder::Builds
end
Gitlab::Seeder.quiet do
- Project.all.sample(10).each do |project|
- project_builds = Gitlab::Seeder::Builds.new(project)
+ Project.all.sample(5).each do |project|
+ project_builds = Gitlab::Seeder::Pipelines.new(project)
project_builds.seed!
end
end
diff --git a/db/migrate/20160801163421_add_expires_at_to_member.rb b/db/migrate/20160801163421_add_expires_at_to_member.rb
new file mode 100644
index 00000000000..8db0fc60c4b
--- /dev/null
+++ b/db/migrate/20160801163421_add_expires_at_to_member.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddExpiresAtToMember < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :members, :expires_at, :date
+ end
+end
diff --git a/db/migrate/20160817133006_add_koding_to_application_settings.rb b/db/migrate/20160817133006_add_koding_to_application_settings.rb
new file mode 100644
index 00000000000..915d3d78e40
--- /dev/null
+++ b/db/migrate/20160817133006_add_koding_to_application_settings.rb
@@ -0,0 +1,10 @@
+class AddKodingToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :application_settings, :koding_enabled, :boolean
+ add_column :application_settings, :koding_url, :string
+ end
+end
diff --git a/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb
new file mode 100644
index 00000000000..0ed538b0df8
--- /dev/null
+++ b/db/migrate/20160818205718_add_expires_at_to_project_group_links.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddExpiresAtToProjectGroupLinks < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ # When a migration requires downtime you **must** uncomment the following
+ # constant and define a short and easy to understand explanation as to why the
+ # migration requires downtime.
+ # DOWNTIME_REASON = ''
+
+ # When using the methods "add_concurrent_index" or "add_column_with_default"
+ # you must disable the use of transactions as these methods can not run in an
+ # existing transaction. When using "add_concurrent_index" make sure that this
+ # method is the _only_ method called in the migration, any other changes
+ # should go in a separate migration. This ensures that upon failure _only_ the
+ # index creation fails and can be retried or reverted easily.
+ #
+ # To disable transactions uncomment the following line and remove these
+ # comments:
+ # disable_ddl_transaction!
+
+ def change
+ add_column :project_group_links, :expires_at, :date
+ end
+end
diff --git a/db/migrate/20160819221631_add_index_to_note_discussion_id.rb b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
new file mode 100644
index 00000000000..b6e8bb18e7b
--- /dev/null
+++ b/db/migrate/20160819221631_add_index_to_note_discussion_id.rb
@@ -0,0 +1,14 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddIndexToNoteDiscussionId < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :notes, :discussion_id
+ end
+end
diff --git a/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb
new file mode 100644
index 00000000000..0c68cf01900
--- /dev/null
+++ b/db/migrate/20160819221833_reset_diff_note_discussion_id_because_it_was_calculated_wrongly.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class ResetDiffNoteDiscussionIdBecauseItWasCalculatedWrongly < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ execute "UPDATE notes SET discussion_id = NULL WHERE discussion_id IS NOT NULL AND type = 'DiffNote'"
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index c74d4688a7d..4947745b232 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20160817154936) do
+ActiveRecord::Schema.define(version: 20160819221833) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -90,6 +90,8 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "enabled_git_access_protocol"
t.boolean "domain_blacklist_enabled", default: false
t.text "domain_blacklist"
+ t.boolean "koding_enabled"
+ t.string "koding_url"
end
create_table "audit_events", force: :cascade do |t|
@@ -569,6 +571,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.string "invite_token"
t.datetime "invite_accepted_at"
t.datetime "requested_at"
+ t.date "expires_at"
end
add_index "members", ["access_level"], name: "index_members_on_access_level", using: :btree
@@ -701,6 +704,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
add_index "notes", ["commit_id"], name: "index_notes_on_commit_id", using: :btree
add_index "notes", ["created_at"], name: "index_notes_on_created_at", using: :btree
+ add_index "notes", ["discussion_id"], name: "index_notes_on_discussion_id", using: :btree
add_index "notes", ["line_code"], name: "index_notes_on_line_code", using: :btree
add_index "notes", ["note"], name: "index_notes_on_note_trigram", using: :gin, opclasses: {"note"=>"gin_trgm_ops"}
add_index "notes", ["noteable_id", "noteable_type"], name: "index_notes_on_noteable_id_and_noteable_type", using: :btree
@@ -785,6 +789,7 @@ ActiveRecord::Schema.define(version: 20160817154936) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "group_access", default: 30, null: false
+ t.date "expires_at"
end
create_table "project_import_data", force: :cascade do |t|
@@ -1151,4 +1156,4 @@ ActiveRecord::Schema.define(version: 20160817154936) do
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
add_foreign_key "u2f_registrations", "users"
-end
+end \ No newline at end of file
diff --git a/doc/api/members.md b/doc/api/members.md
index d002e6eaf89..fd6d728dad2 100644
--- a/doc/api/members.md
+++ b/doc/api/members.md
@@ -86,7 +86,8 @@ Example response:
"name": "Raymond Smith",
"state": "active",
"created_at": "2012-10-22T14:13:35Z",
- "access_level": 30
+ "access_level": 30,
+ "expires_at": null
}
```
@@ -106,6 +107,7 @@ POST /projects/:id/members
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the new member |
| `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=30
@@ -141,6 +143,7 @@ PUT /projects/:id/members/:user_id
| `id` | integer/string | yes | The group/project ID or path |
| `user_id` | integer | yes | The user ID of the member |
| `access_level` | integer | yes | A valid access level |
+| `expires_at` | string | no | A date string in the format YEAR-MONTH-DAY |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/groups/:id/members/:user_id?access_level=40
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index d90d7aca4fd..20cd88c8d20 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -67,6 +67,8 @@ use following Markdown code to embed the est coverage report into `README.md`:
![coverage](http://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)
```
+The latest successful pipeline will be used to read the test coverage value.
+
[builds]: #builds
[jobs]: yaml/README.md#jobs
[stages]: yaml/README.md#stages
diff --git a/doc/integration/README.md b/doc/integration/README.md
index ddbd570ac6c..70895abbcad 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -15,6 +15,7 @@ See the documentation below for details on how to configure these services.
- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
- [Akismet](akismet.md) Configure Akismet to stop spam
+- [Koding](koding.md) Configure Koding to use IDE integration
GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
diff --git a/doc/integration/img/koding_build-in-progress.png b/doc/integration/img/koding_build-in-progress.png
new file mode 100644
index 00000000000..f8cc81834c4
--- /dev/null
+++ b/doc/integration/img/koding_build-in-progress.png
Binary files differ
diff --git a/doc/integration/img/koding_build-logs.png b/doc/integration/img/koding_build-logs.png
new file mode 100644
index 00000000000..a04cd5aff99
--- /dev/null
+++ b/doc/integration/img/koding_build-logs.png
Binary files differ
diff --git a/doc/integration/img/koding_build-success.png b/doc/integration/img/koding_build-success.png
new file mode 100644
index 00000000000..2a0dd296480
--- /dev/null
+++ b/doc/integration/img/koding_build-success.png
Binary files differ
diff --git a/doc/integration/img/koding_commit-koding.yml.png b/doc/integration/img/koding_commit-koding.yml.png
new file mode 100644
index 00000000000..3e133c50327
--- /dev/null
+++ b/doc/integration/img/koding_commit-koding.yml.png
Binary files differ
diff --git a/doc/integration/img/koding_different-stack-on-mr-try.png b/doc/integration/img/koding_different-stack-on-mr-try.png
new file mode 100644
index 00000000000..fd25e32f648
--- /dev/null
+++ b/doc/integration/img/koding_different-stack-on-mr-try.png
Binary files differ
diff --git a/doc/integration/img/koding_edit-on-ide.png b/doc/integration/img/koding_edit-on-ide.png
new file mode 100644
index 00000000000..fd5aaff75f5
--- /dev/null
+++ b/doc/integration/img/koding_edit-on-ide.png
Binary files differ
diff --git a/doc/integration/img/koding_enable-koding.png b/doc/integration/img/koding_enable-koding.png
new file mode 100644
index 00000000000..c0ae0ee9918
--- /dev/null
+++ b/doc/integration/img/koding_enable-koding.png
Binary files differ
diff --git a/doc/integration/img/koding_landing.png b/doc/integration/img/koding_landing.png
new file mode 100644
index 00000000000..7c629d9b05e
--- /dev/null
+++ b/doc/integration/img/koding_landing.png
Binary files differ
diff --git a/doc/integration/img/koding_open-gitlab-from-koding.png b/doc/integration/img/koding_open-gitlab-from-koding.png
new file mode 100644
index 00000000000..c958cf8f224
--- /dev/null
+++ b/doc/integration/img/koding_open-gitlab-from-koding.png
Binary files differ
diff --git a/doc/integration/img/koding_run-in-ide.png b/doc/integration/img/koding_run-in-ide.png
new file mode 100644
index 00000000000..f91ee0f74cc
--- /dev/null
+++ b/doc/integration/img/koding_run-in-ide.png
Binary files differ
diff --git a/doc/integration/img/koding_run-mr-in-ide.png b/doc/integration/img/koding_run-mr-in-ide.png
new file mode 100644
index 00000000000..502817a2a46
--- /dev/null
+++ b/doc/integration/img/koding_run-mr-in-ide.png
Binary files differ
diff --git a/doc/integration/img/koding_set-up-ide.png b/doc/integration/img/koding_set-up-ide.png
new file mode 100644
index 00000000000..7f408c980b5
--- /dev/null
+++ b/doc/integration/img/koding_set-up-ide.png
Binary files differ
diff --git a/doc/integration/img/koding_stack-import.png b/doc/integration/img/koding_stack-import.png
new file mode 100644
index 00000000000..2a4e3c87fc8
--- /dev/null
+++ b/doc/integration/img/koding_stack-import.png
Binary files differ
diff --git a/doc/integration/img/koding_start-build.png b/doc/integration/img/koding_start-build.png
new file mode 100644
index 00000000000..52159440f62
--- /dev/null
+++ b/doc/integration/img/koding_start-build.png
Binary files differ
diff --git a/doc/integration/koding-usage.md b/doc/integration/koding-usage.md
new file mode 100644
index 00000000000..bb74badce66
--- /dev/null
+++ b/doc/integration/koding-usage.md
@@ -0,0 +1,122 @@
+# Koding & GitLab
+
+This document will guide you through using Koding integration on GitLab in
+detail. For configuring and installing please follow [this](koding.md) guide.
+
+You can use Koding integration to run and develop your projects on GitLab. This
+will allow you and the users to test your project without leaving the browser.
+Koding handles projects as stacks which are basic recipes to define your
+environment for your project. With this integration you can automatically
+create a proper stack template for your projects. Currently auto-generated
+stack templates are designed to work with AWS which requires a valid AWS
+credential to be able to use these stacks. You can find more information about
+stacks and the other providers that you can use on Koding from
+[here](https://www.koding.com/docs).
+
+
+# Enable Integration
+
+You can enable Koding integration by providing the running Koding instance URL
+in Application Settings;
+
+ - Open **Admin area > Settings** (`/admin/application_settings`).
+
+![Enable Koding](help/integration/img/koding_enable-koding.png)
+
+Once enabled you will see `Koding` link on your sidebar which leads you to
+Koding Landing page
+
+![Koding Landing](help/integration/img/koding_landing.png)
+
+You can navigate to running Koding instance from here. For more information and
+details about configuring integration please follow [this](koding.md) guide.
+
+
+# Set up Koding on Projects
+
+Once it's enabled, you will see some integration buttons on Project pages,
+Merge Requests etc. To get started working on a specific project you first need
+to create a `.koding.yml` file under your project root. You can easily do that
+by using `Set Up Koding` button which will be visible on every project's
+landing page;
+
+![Set Up Koding](help/integration/img/koding_set-up-ide.png)
+
+Once you click this will open a New File page on GitLab with auto-generated
+`.koding.yml` content based on your server and repository configuration.
+
+![Commit .koding.yml](help/integration/img/koding_commit-koding.yml.png)
+
+
+# Run a project on Koding
+
+If there is `.koding.yml` exists in your project root, you will see
+`Run in IDE (Koding)` button in your project landing page. You can initiate the
+process from here.
+
+![Run on Koding](help/integration/img/koding_run-in-ide.png)
+
+This will open Koding defined in the settings in a new window and will start
+importing the project's stack file;
+
+![Import Stack](help/integration/img/koding_stack-import.png)
+
+You should see the details of your repository imported into your Koding
+instance. Once it's completed it will lead you to the Stack Editor and from
+there you can start using your new stack integrated with your project on your
+GitLab instance. For details about what's next you can follow
+[this](https://www.koding.com/docs/creating-an-aws-stack) guide from 8. step.
+
+Once stack initialized you will see the `README.md` content from your project
+in `Stack Build` wizard, this wizard will let you to build the stack and import
+your project into it. **Once it's completed it will automatically open the
+related vm instead of importing from scratch**
+
+![Stack Building](help/integration/img/koding_start-build.png)
+
+This will take time depending on the required environment.
+
+![Stack Building in Progress](help/integration/img/koding_build-in-progress.png)
+
+It usually takes ~4 min. to make it ready with a `t2.nano` instance on given
+AWS region. (`t2.nano` is default vm type on auto-generated stack template
+which can be manually changed)
+
+![Stack Building Success](help/integration/img/koding_build-success.png)
+
+You can check out the `Build Logs` from this success modal as well;
+
+![Stack Build Logs](help/integration/img/koding_build-logs.png)
+
+You can now `Start Coding`!
+
+![Edit On IDE](help/integration/img/koding_edit-on-ide.png)
+
+
+# Try a Merge Request on IDE
+
+It's also possible to try a change on IDE before merging it. This flow only
+enabled if the target project has `.koding.yml` in it's target branch. You
+should see the alternative version of `Run in IDE (Koding)` button in merge
+request pages as well;
+
+![Run in IDE on MR](help/integration/img/koding_run-mr-in-ide.png)
+
+This will again take you to Koding with proper arguments passed, which will
+allow Koding to modify the stack template provided by target branch. You can
+see the difference;
+
+![Different Branch for MR](help/integration/img/koding_different-stack-on-mr-try.png)
+
+The flow for the branch stack is also same with the regular project flow.
+
+
+# Open GitLab from Koding
+
+Since stacks generated with import flow defined in previous steps, they have
+information about the repository they are belonging to. By using this
+information you can access to related GitLab page from stacks on your sidebar
+on Koding.
+
+![Open GitLab from Koding](help/integration/img/koding_open-gitlab-from-koding.png)
+
diff --git a/doc/integration/koding.md b/doc/integration/koding.md
new file mode 100644
index 00000000000..53450b6d048
--- /dev/null
+++ b/doc/integration/koding.md
@@ -0,0 +1,239 @@
+# Koding & GitLab
+
+This document will guide you through installing and configuring Koding with
+GitLab.
+
+First of all, to be able to use Koding and GitLab together you will need public
+access to your server. This allows you to use single sign-on from GitLab to
+Koding and using vms from cloud providers like AWS. Koding has a registry for
+VMs, called Kontrol and it runs on the same server as Koding itself, VMs from
+cloud providers register themselves to Kontrol via the agent that we put into
+provisioned VMs. This agent is called Klient and it provides Koding to access
+and manage the target machine.
+
+Kontrol and Klient are based on another technology called
+[Kite](github.com/koding/kite), that we have written at Koding. Which is a
+microservice framework that allows you to develop microservices easily.
+
+
+## Requirements
+
+### Hardware
+
+Minimum requirements are;
+
+ - 2 cores CPU
+ - 3G RAM
+ - 10G Storage
+
+If you plan to use AWS to install Koding it is recommended that you use at
+least a `c3.xlarge` instance.
+
+### Software
+
+ - [git](https://git-scm.com)
+ - [docker](https://www.docker.com)
+ - [docker-compose](https://www.docker.com/products/docker-compose)
+
+Koding can run on most of the UNIX based operating systems, since it's shipped
+as containerized with Docker support, it can work on any operating system that
+supports Docker.
+
+Required services are;
+
+ - PostgreSQL # Kontrol and Service DB provider
+ - MongoDB # Main DB provider the application
+ - Redis # In memory DB used by both application and services
+ - RabbitMQ # Message Queue for both application and services
+
+which are also provided as a Docker container by Koding.
+
+
+## Getting Started with Development Versions
+
+
+### Koding
+
+You can run `docker-compose` environment for developing koding by
+executing commands in the following snippet.
+
+```bash
+git clone https://github.com/koding/koding.git
+cd koding
+docker-compose up
+```
+
+This should start koding on `localhost:8090`.
+
+By default there is no team exists in Koding DB. You'll need to create a team
+called `gitlab` which is the default team name for GitLab integration in the
+configuration. To make things in order it's recommended to create the `gitlab`
+team first thing after setting up Koding.
+
+
+### GitLab
+
+To install GitLab to your environment for development purposes it's recommended
+to use GitLab Development Kit which you can get it from
+[here](https://gitlab.com/gitlab-org/gitlab-development-kit).
+
+After all those steps, gitlab should be running on `localhost:3000`
+
+
+## Integration
+
+Integration includes following components;
+
+ - Single Sign On with OAuth from GitLab to Koding
+ - System Hook integration for handling GitLab events on Koding
+ (`project_created`, `user_joined` etc.)
+ - Service endpoints for importing/executing stacks from GitLab to Koding
+ (`Run/Try on IDE (Koding)` buttons on GitLab Projects, Issues, MRs)
+
+As it's pointed out before, you will need public access to this machine that
+you've installed Koding and GitLab on. Better to use a domain but a static IP
+is also fine.
+
+For IP based installation you can use [xip.io](https://xip.io) service which is
+free and provides DNS resolution to IP based requests like following;
+
+ - 127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - foo.bar.baz.127.0.0.1.xip.io -> resolves to 127.0.0.1
+ - and so on...
+
+As Koding needs subdomains for team names; `foo.127.0.0.1.xip.io` requests for
+a running koding instance on `127.0.0.1` server will be handled as `foo` team
+requests.
+
+
+### GitLab Side
+
+You need to enable Koding integration from Settings under Admin Area. To do
+that login with an Admin account and do followings;
+
+ - open [http://127.0.0.1:3000/admin/application_settings](http://127.0.0.1:3000/admin/application_settings)
+ - scroll to bottom of the page until Koding section
+ - check `Enable Koding` checkbox
+ - provide GitLab team page for running Koding instance as `Koding URL`*
+
+* For `Koding URL` you need to provide the gitlab integration enabled team on
+your Koding installation. Team called `gitlab` has integration on Koding out
+of the box, so if you didn't change anything your team on Koding should be
+`gitlab`.
+
+So, if your Koding is running on `http://1.2.3.4.xip.io:8090` your URL needs
+to be `http://gitlab.1.2.3.4.xip.io:8090`. You need to provide the same host
+with your Koding installation here.
+
+
+#### Registering Koding for OAuth integration
+
+We need `Application ID` and `Secret` to enable login to Koding via GitLab
+feature and to do that you need to register running Koding as a new application
+to your running GitLab application. Follow
+[these](http://docs.gitlab.com/ce/integration/oauth_provider.html) steps to
+enable this integration.
+
+Redirect URI should be `http://gitlab.127.0.0.1:8090/-/oauth/gitlab/callback`
+which again you need to _replace `127.0.0.1` with your instance public IP._
+
+Take a copy of `Application ID` and `Secret` that is generated by the GitLab
+application, we will need those on _Koding Part_ of this guide.
+
+
+#### Registering system hooks to Koding (optional)
+
+Koding can take actions based on the events generated by GitLab application.
+This feature is still in progress and only following events are processed by
+Koding at the moment;
+
+ - user_create
+ - user_destroy
+
+All system events are handled but not implemented on Koding side.
+
+To enable this feature you need to provide a `URL` and a `Secret Token` to your
+GitLab application. Open your admin area on your GitLab app from
+[http://127.0.0.1:3000/admin/hooks](http://127.0.0.1:3000/admin/hooks)
+and provide `URL` as `http://gitlab.127.0.0.1:8090/-/api/gitlab` which is the
+endpoint to handle GitLab events on Koding side. Provide a `Secret Token` and
+keep a copy of it, we will need it on _Koding Part_ of this guide.
+
+_(replace `127.0.0.1` with your instance public IP)_
+
+
+### Koding Part
+
+If you followed the steps in GitLab part we should have followings to enable
+Koding part integrations;
+
+ - `Application ID` and `Secret` for OAuth integration
+ - `Secret Token` for system hook integration
+ - Public address of running GitLab instance
+
+
+#### Start Koding with GitLab URL
+
+Now we need to configure Koding with all this information to get things ready.
+If it's already running please stop koding first.
+
+##### From command-line
+
+Replace followings with the ones you got from GitLab part of this guide;
+
+```bash
+cd koding
+docker-compose run \
+ --service-ports backend \
+ /opt/koding/scripts/bootstrap-container build \
+ --host=**YOUR_IP**.xip.io \
+ --gitlabHost=**GITLAB_IP** \
+ --gitlabPort=**GITLAB_PORT** \
+ --gitlabToken=**SECRET_TOKEN** \
+ --gitlabAppId=**APPLICATION_ID** \
+ --gitlabAppSecret=**SECRET**
+```
+
+##### By updating configuration
+
+Alternatively you can update `gitlab` section on
+`config/credentials.default.coffee` like following;
+
+```
+gitlab =
+ host: '**GITLAB_IP**'
+ port: '**GITLAB_PORT**'
+ applicationId: '**APPLICATION_ID**'
+ applicationSecret: '**SECRET**'
+ team: 'gitlab'
+ redirectUri: ''
+ systemHookToken: '**SECRET_TOKEN**'
+ hooksEnabled: yes
+```
+
+and start by only providing the `host`;
+
+```bash
+cd koding
+docker-compose run \
+ --service-ports backend \
+ /opt/koding/scripts/bootstrap-container build \
+ --host=**YOUR_IP**.xip.io \
+```
+
+#### Enable Single Sign On
+
+Once you restarted your Koding and logged in with your username and password
+you need to activate oauth authentication for your user. To do that
+
+ - Navigate to Dashboard on Koding from;
+ `http://gitlab.**YOUR_IP**.xip.io:8090/Home/my-account`
+ - Scroll down to Integrations section
+ - Click on toggle to turn On integration in GitLab integration section
+
+This will redirect you to your GitLab instance and will ask your permission (
+if you are not logged in to GitLab at this point you will be redirected after
+login) once you accept you will be redirected to your Koding instance.
+
+From now on you can login by using `SIGN IN WITH GITLAB` button on your Login
+screen in your Koding instance.
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index 84c624cbcb7..253fff50bdd 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -46,7 +46,7 @@ sudo -u git -H git checkout 8-11-stable-ee
```bash
cd /home/git/gitlab-shell
sudo -u git -H git fetch --all --tags
-sudo -u git -H git checkout v3.3.3
+sudo -u git -H git checkout v3.4.0
```
### 5. Update gitlab-workhorse
diff --git a/doc/workflow/share_projects_with_other_groups.md b/doc/workflow/share_projects_with_other_groups.md
index 4c59f59c587..8e50cb03e63 100644
--- a/doc/workflow/share_projects_with_other_groups.md
+++ b/doc/workflow/share_projects_with_other_groups.md
@@ -1,22 +1,24 @@
# Share Projects with other Groups
-In GitLab Enterprise Edition you can share projects with other groups.
-This makes it possible to add a group of users to a project with a single action.
+You can share projects with other groups. This makes it possible to add a group of users
+to a project with a single action.
## Groups as collections of users
-In GitLab Community Edition groups are used primarily to [create collections of projects](groups.md).
-In GitLab Enterprise Edition you can also take advantage of the fact that groups define collections of _users_, namely the group members.
+Groups are used primarily to [create collections of projects](groups.md), but you can also
+take advantage of the fact that groups define collections of _users_, namely the group
+members.
## Sharing a project with a group of users
-The primary mechanism to give a group of users, say 'Engineering', access to a project, say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project Acme'.
-But what if 'Project Acme' already belongs to another group, say 'Open Source'?
-This is where the (Enterprise Edition only) group sharing feature can be of use.
+The primary mechanism to give a group of users, say 'Engineering', access to a project,
+say 'Project Acme', in GitLab is to make the 'Engineering' group the owner of 'Project
+Acme'. But what if 'Project Acme' already belongs to another group, say 'Open Source'?
+This is where the group sharing feature can be of use.
To share 'Project Acme' with the 'Engineering' group, go to the project settings page for 'Project Acme' and use the left navigation menu to go to the 'Groups' section.
-![The 'Groups' section in the project settings screen (Enterprise Edition only)](groups/share_project_with_groups.png)
+![The 'Groups' section in the project settings screen](groups/share_project_with_groups.png)
Now you can add the 'Engineering' group with the maximum access level of your choice.
After sharing 'Project Acme' with 'Engineering', the project is listed on the group dashboard.
diff --git a/features/steps/group/members.rb b/features/steps/group/members.rb
index dfa2fa75def..e9b45823c67 100644
--- a/features/steps/group/members.rb
+++ b/features/steps/group/members.rb
@@ -116,8 +116,8 @@ class Spinach::Features::GroupMembers < Spinach::FeatureSteps
member = mary_jane_member
page.within "#group_member_#{member.id}" do
- click_button "Edit access level"
- select 'Developer', from: 'group_member_access_level'
+ click_button 'Edit'
+ select 'Developer', from: "member_access_level_#{member.id}"
click_on 'Save'
end
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 841d191d55b..bb79424ee08 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -44,7 +44,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I should see its content with new lines preserved at end of file' do
- expect(evaluate_script('blob.editor.getValue()')).to eq "Sample\n\n\n"
+ expect(evaluate_script('ace.edit("editor").getValue()')).to eq "Sample\n\n\n"
end
step 'I click link "Raw"' do
@@ -65,7 +65,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I can edit code' do
set_new_content
- expect(evaluate_script('blob.editor.getValue()')).to eq new_gitignore_content
+ expect(evaluate_script('ace.edit("editor").getValue()')).to eq new_gitignore_content
end
step 'I edit code' do
@@ -74,7 +74,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step 'I edit code with new lines at end of file' do
- execute_script('blob.editor.setValue("Sample\n\n\n")')
+ execute_script('ace.edit("editor").setValue("Sample\n\n\n")')
end
step 'I fill the new file name' do
@@ -378,7 +378,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
private
def set_new_content
- execute_script("blob.editor.setValue('#{new_gitignore_content}')")
+ execute_script("ace.edit('editor').setValue('#{new_gitignore_content}')")
end
# Content of the gitignore file on the seed repository.
diff --git a/features/steps/project/team_management.rb b/features/steps/project/team_management.rb
index f32576d2cb1..e920f5a706b 100644
--- a/features/steps/project/team_management.rb
+++ b/features/steps/project/team_management.rb
@@ -65,8 +65,8 @@ class Spinach::Features::ProjectTeamManagement < Spinach::FeatureSteps
user = User.find_by(name: 'Dmitriy')
project_member = project.project_members.find_by(user_id: user.id)
page.within "#project_member_#{project_member.id}" do
- click_button "Edit access level"
- select "Reporter", from: "project_member_access_level"
+ click_button 'Edit'
+ select "Reporter", from: "member_access_level_#{project_member.id}"
click_button "Save"
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index fcb0b12c191..3f050a8fd81 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -97,6 +97,10 @@ module API
member = options[:member] || options[:members].find { |m| m.user_id == user.id }
member.access_level
end
+ expose :expires_at do |user, options|
+ member = options[:member] || options[:members].find { |m| m.user_id == user.id }
+ member.expires_at
+ end
end
class AccessRequester < UserBasic
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 2fae83f60b2..94c16710d9a 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -49,6 +49,7 @@ module API
# id (required) - The group/project ID
# user_id (required) - The user ID of the new member
# access_level (required) - A valid access level
+ # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
#
# Example Request:
# POST /groups/:id/members
@@ -72,7 +73,7 @@ module API
conflict!('Member already exists') if source_type == 'group' && member
unless member
- source.add_user(params[:user_id], params[:access_level], current_user)
+ source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
member = source.members.find_by(user_id: params[:user_id])
end
@@ -81,7 +82,7 @@ module API
else
# Since `source.add_user` doesn't return a member object, we have to
# build a new one and populate its errors in order to render them.
- member = source.members.build(attributes_for_keys([:user_id, :access_level]))
+ member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
member.valid? # populate the errors
# This is to ensure back-compatibility but 400 behavior should be used
@@ -97,6 +98,7 @@ module API
# id (required) - The group/project ID
# user_id (required) - The user ID of the member
# access_level (required) - A valid access level
+ # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
#
# Example Request:
# PUT /groups/:id/members/:user_id
@@ -107,8 +109,9 @@ module API
required_attributes! [:user_id, :access_level]
member = source.members.find_by!(user_id: params[:user_id])
+ attrs = attributes_for_keys [:access_level, :expires_at]
- if member.update_attributes(access_level: params[:access_level])
+ if member.update_attributes(attrs)
present member.user, with: Entities::Member, member: member
else
# This is to ensure back-compatibility but 400 behavior should be used
diff --git a/lib/extracts_path.rb b/lib/extracts_path.rb
index 84688f6646e..a293fa2752f 100644
--- a/lib/extracts_path.rb
+++ b/lib/extracts_path.rb
@@ -94,7 +94,9 @@ module ExtractsPath
@options = params.select {|key, value| allowed_options.include?(key) && !value.blank? }
@options = HashWithIndifferentAccess.new(@options)
- @id = Addressable::URI.normalize_component(get_id)
+ @id = params[:id] || params[:ref]
+ @id += "/" + params[:path] unless params[:path].blank?
+
@ref, @path = extract_ref(@id)
@repo = @project.repository
if @options[:extended_sha1].blank?
@@ -116,12 +118,4 @@ module ExtractsPath
def tree
@tree ||= @repo.tree(@commit.id, @path)
end
-
- private
-
- def get_id
- id = params[:id] || params[:ref]
- id += "/" + params[:path] unless params[:path].blank?
- id
- end
end
diff --git a/lib/gitlab/badge/coverage/report.rb b/lib/gitlab/badge/coverage/report.rb
index 3d56ea3e47a..95d925dc7f3 100644
--- a/lib/gitlab/badge/coverage/report.rb
+++ b/lib/gitlab/badge/coverage/report.rb
@@ -13,8 +13,7 @@ module Gitlab
@job = job
@pipeline = @project.pipelines
- .where(ref: @ref)
- .where(sha: @project.commit(@ref).try(:sha))
+ .latest_successful_for(@ref)
.first
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 735331df66c..27acd817e51 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -30,6 +30,7 @@ module Gitlab
signup_enabled: Settings.gitlab['signup_enabled'],
signin_enabled: Settings.gitlab['signin_enabled'],
gravatar_enabled: Settings.gravatar['enabled'],
+ koding_enabled: false,
sign_in_text: nil,
after_sign_up_text: nil,
help_page_text: nil,
diff --git a/lib/gitlab/diff/position.rb b/lib/gitlab/diff/position.rb
index 2fdcf8d7838..ecf62dead35 100644
--- a/lib/gitlab/diff/position.rb
+++ b/lib/gitlab/diff/position.rb
@@ -139,13 +139,19 @@ module Gitlab
private
def find_diff_file(repository)
- diffs = Gitlab::Git::Compare.new(
- repository.raw_repository,
- start_sha,
- head_sha
- ).diffs(paths: paths)
+ # We're at the initial commit, so just get that as we can't compare to anything.
+ if Gitlab::Git.blank_ref?(start_sha)
+ compare = Gitlab::Git::Commit.find(repository.raw_repository, head_sha)
+ else
+ compare = Gitlab::Git::Compare.new(
+ repository.raw_repository,
+ start_sha,
+ head_sha
+ )
+ end
+
+ diff = compare.diffs(paths: paths).first
- diff = diffs.first
return unless diff
Gitlab::Diff::File.new(diff, repository: repository, diff_refs: diff_refs)
diff --git a/lib/gitlab/email/handler.rb b/lib/gitlab/email/handler.rb
index bd3267e2a80..5cf9d5ebe28 100644
--- a/lib/gitlab/email/handler.rb
+++ b/lib/gitlab/email/handler.rb
@@ -4,7 +4,8 @@ require 'gitlab/email/handler/create_issue_handler'
module Gitlab
module Email
module Handler
- HANDLERS = [CreateNoteHandler, CreateIssueHandler]
+ # The `CreateIssueHandler` feature is disabled for the time being.
+ HANDLERS = [CreateNoteHandler]
def self.for(mail, mail_key)
HANDLERS.find do |klass|
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index d13fe0ef8a9..e59ead5d76c 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -7,7 +7,7 @@ module Gitlab
# @param cmd [Array<String>]
# @return [Boolean]
def system_silent(cmd)
- Popen::popen(cmd).last.zero?
+ Popen.popen(cmd).last.zero?
end
def force_utf8(str)
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 44128a43362..a121cb2fc97 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -237,6 +237,56 @@ describe AutocompleteController do
end
end
+ context 'authorized projects apply limit' do
+ before do
+ authorized_project2 = create(:project)
+ authorized_project3 = create(:project)
+
+ authorized_project.team << [user, :master]
+ authorized_project2.team << [user, :master]
+ authorized_project3.team << [user, :master]
+
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+ end
+
+ describe 'GET #projects with project ID' do
+ before do
+ get(:projects, project_id: project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body).to be_kind_of(Array)
+ expect(body.size).to eq 3 # Of a total of 4
+ end
+ end
+ end
+
+ context 'authorized projects with offset' do
+ before do
+ authorized_project2 = create(:project)
+ authorized_project3 = create(:project)
+
+ authorized_project.team << [user, :master]
+ authorized_project2.team << [user, :master]
+ authorized_project3.team << [user, :master]
+ end
+
+ describe 'GET #projects with project ID and offset_id' do
+ before do
+ get(:projects, project_id: project.id, offset_id: authorized_project.id)
+ end
+
+ let(:body) { JSON.parse(response.body) }
+
+ it do
+ expect(body.detect { |item| item['id'] == 0 }).to be_nil # 'No project' is not there
+ expect(body.detect { |item| item['id'] == authorized_project.id }).to be_nil # Offset project is not there either
+ end
+ end
+ end
+
context 'authorized projects without admin_issue ability' do
before(:each) do
authorized_project.team << [user, :guest]
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 8910c50c294..5d777895542 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -572,6 +572,18 @@ describe 'Issue Boards', feature: true, js: true do
end
end
+ context 'keyboard shortcuts' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'allows user to use keyboard shortcuts' do
+ find('.boards-list').native.send_keys('i')
+ expect(page).to have_content('New Issue')
+ end
+ end
+
context 'signed out user' do
before do
logout
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index cb445e22af0..2e595959f04 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -525,7 +525,7 @@ describe 'Issues', feature: true do
end
end
- describe 'new issue by email' do
+ xdescribe 'new issue by email' do
shared_examples 'show the email in the modal' do
before do
stub_incoming_email_setting(enabled: true, address: "p+%{key}@gl.ab")
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index af86d3c338a..5972e7f31c2 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -4,12 +4,6 @@ feature 'test coverage badge' do
given!(:user) { create(:user) }
given!(:project) { create(:project, :private) }
- given!(:pipeline) do
- create(:ci_pipeline, project: project,
- ref: 'master',
- sha: project.commit.id)
- end
-
context 'when user has access to view badge' do
background do
project.team << [user, :developer]
@@ -17,8 +11,10 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge image for pipeline' do
- create_job(coverage: 100, name: 'test:1')
- create_job(coverage: 90, name: 'test:2')
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: 100, name: 'test:1')
+ create_build(pipeline, coverage: 90, name: 'test:2')
+ end
show_test_coverage_badge
@@ -26,9 +22,11 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge for specific job' do
- create_job(coverage: 50, name: 'test:1')
- create_job(coverage: 50, name: 'test:2')
- create_job(coverage: 85, name: 'coverage')
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: 50, name: 'test:1')
+ create_build(pipeline, coverage: 50, name: 'test:2')
+ create_build(pipeline, coverage: 85, name: 'coverage')
+ end
show_test_coverage_badge(job: 'coverage')
@@ -36,7 +34,9 @@ feature 'test coverage badge' do
end
scenario 'user requests coverage badge for pipeline without coverage' do
- create_job(coverage: nil, name: 'test')
+ create_pipeline do |pipeline|
+ create_build(pipeline, coverage: nil, name: 'test')
+ end
show_test_coverage_badge
@@ -54,10 +54,19 @@ feature 'test coverage badge' do
end
end
- def create_job(coverage:, name:)
- create(:ci_build, name: name,
- coverage: coverage,
- pipeline: pipeline)
+ def create_pipeline
+ opts = { project: project, ref: 'master', sha: project.commit.id }
+
+ create(:ci_pipeline, opts).tap do |pipeline|
+ yield pipeline
+ pipeline.build_updated
+ end
+ end
+
+ def create_build(pipeline, coverage:, name:)
+ opts = { pipeline: pipeline, coverage: coverage, name: name }
+
+ create(:ci_build, :success, opts)
end
def show_test_coverage_badge(job: nil)
diff --git a/spec/features/projects/branches/delete_spec.rb b/spec/features/projects/branches/delete_spec.rb
new file mode 100644
index 00000000000..63878c55421
--- /dev/null
+++ b/spec/features/projects/branches/delete_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+feature 'Delete branch', feature: true, js: true do
+ include WaitForAjax
+
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_branches_path(project.namespace, project)
+ end
+
+ it 'destroys tooltip' do
+ first('.remove-row').hover
+ expect(page).to have_selector('.tooltip')
+
+ first('.remove-row').click
+ wait_for_ajax
+
+ expect(page).not_to have_selector('.tooltip')
+ end
+end
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 79abba21854..1b14945bf0a 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -20,7 +20,7 @@ describe 'Branches', feature: true do
describe 'Find branches' do
it 'shows filtered branches', js: true do
- visit namespace_project_branches_path(project.namespace, project, project.id)
+ visit namespace_project_branches_path(project.namespace, project)
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
diff --git a/spec/features/projects/commits/cherry_pick_spec.rb b/spec/features/projects/commits/cherry_pick_spec.rb
index 1b4ff6b6f1b..e45e3a36d01 100644
--- a/spec/features/projects/commits/cherry_pick_spec.rb
+++ b/spec/features/projects/commits/cherry_pick_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+include WaitForAjax
describe 'Cherry-pick Commits' do
let(:project) { create(:project) }
@@ -8,12 +9,11 @@ describe 'Cherry-pick Commits' do
before do
login_as :user
project.team << [@user, :master]
- visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 })
+ visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
end
context "I cherry-pick a commit" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
expect(page).not_to have_content('v1.0.0') # Only branches, not tags
page.within('#modal-cherry-pick-commit') do
@@ -26,7 +26,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a merge commit" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_merge.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
@@ -38,7 +37,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit that was previously cherry-picked" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
uncheck 'create_merge_request'
@@ -56,7 +54,6 @@ describe 'Cherry-pick Commits' do
context "I cherry-pick a commit in a new merge request" do
it do
- visit namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
find("a[href='#modal-cherry-pick-commit']").click
page.within('#modal-cherry-pick-commit') do
click_button 'Cherry-pick'
@@ -64,4 +61,28 @@ describe 'Cherry-pick Commits' do
expect(page).to have_content('The commit has been successfully cherry-picked. You can now submit a merge request to get this change into the original branch.')
end
end
+
+ context "I cherry-pick a commit from a different branch", js: true do
+ it do
+ find('.commit-action-buttons a.dropdown-toggle').click
+ find(:css, "a[href='#modal-cherry-pick-commit']").click
+
+ page.within('#modal-cherry-pick-commit') do
+ click_button 'master'
+ end
+
+ wait_for_ajax
+
+ page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
+ click_link 'feature'
+ end
+
+ page.within('#modal-cherry-pick-commit') do
+ uncheck 'create_merge_request'
+ click_button 'Cherry-pick'
+ end
+
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
new file mode 100644
index 00000000000..1a71a03fbd9
--- /dev/null
+++ b/spec/features/projects/group_links_spec.rb
@@ -0,0 +1,32 @@
+require 'spec_helper'
+
+feature 'Project group links', feature: true, js: true do
+ include Select2Helper
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:group) { create(:group) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ context 'setting an expiration date for a group link' do
+ before do
+ visit namespace_project_group_links_path(project.namespace, project)
+
+ select2 group.id, from: '#link_group_id'
+ fill_in 'expires_at', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
+ page.find('body').click
+ click_on 'Share'
+ end
+
+ it 'shows the expiration time with a warning class' do
+ page.within('.enabled-groups') do
+ expect(page).to have_content('expires in 4 days')
+ expect(page).to have_selector('.text-warning')
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
new file mode 100644
index 00000000000..430c384ac2e
--- /dev/null
+++ b/spec/features/projects/members/master_adds_member_with_expiration_date_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Master adds member with expiration date', feature: true, js: true do
+ include Select2Helper
+ include ActiveSupport::Testing::TimeHelpers
+
+ let(:master) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:new_member) { create(:user) }
+
+ background do
+ project.team << [master, :master]
+ login_as(master)
+ end
+
+ scenario 'expiration date is displayed in the members list' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.users-project-form' do
+ select2(new_member.id, from: '#user_ids', multiple: true)
+ fill_in 'expires_at', with: '2016-08-10'
+ click_on 'Add users to project'
+ end
+
+ page.within '.project_member:first-child' do
+ expect(page).to have_content('Expires in 4 days')
+ end
+ end
+ end
+
+ scenario 'change expiration date' do
+ travel_to Time.zone.parse('2016-08-06 08:00') do
+ project.team.add_users([new_member.id], :developer, expires_at: '2016-09-06')
+ visit namespace_project_project_members_path(project.namespace, project)
+
+ page.within '.project_member:first-child' do
+ click_on 'Edit'
+ fill_in 'Access expiration date', with: '2016-08-09'
+ click_on 'Save'
+ expect(page).to have_content('Expires in 3 days')
+ end
+ end
+ end
+end
diff --git a/spec/features/protected_branches/access_control_ce_spec.rb b/spec/features/protected_branches/access_control_ce_spec.rb
new file mode 100644
index 00000000000..395c61a4743
--- /dev/null
+++ b/spec/features/protected_branches/access_control_ce_spec.rb
@@ -0,0 +1,71 @@
+RSpec.shared_examples "protected branches > access control > CE" do
+ ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected branches that #{access_type_name} can push to" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ within('.new_protected_branch') do
+ allowed_to_push_button = find(".js-allowed-to-push")
+
+ unless allowed_to_push_button.text == access_type_name
+ allowed_to_push_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
+ end
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected branches so that #{access_type_name} can push to them" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+
+ within(".protected-branches-list") do
+ find(".js-allowed-to-push").click
+ within('.js-allowed-to-push-container') { click_on access_type_name }
+ end
+
+ wait_for_ajax
+ expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+
+ ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
+ it "allows creating protected branches that #{access_type_name} can merge to" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ within('.new_protected_branch') do
+ allowed_to_merge_button = find(".js-allowed-to-merge")
+
+ unless allowed_to_merge_button.text == access_type_name
+ allowed_to_merge_button.click
+ within(".dropdown.open .dropdown-menu") { click_on access_type_name }
+ end
+ end
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
+ end
+
+ it "allows updating protected branches so that #{access_type_name} can merge to them" do
+ visit namespace_project_protected_branches_path(project.namespace, project)
+ set_protected_branch_name('master')
+ click_on "Protect"
+
+ expect(ProtectedBranch.count).to eq(1)
+
+ within(".protected-branches-list") do
+ find(".js-allowed-to-merge").click
+ within('.js-allowed-to-merge-container') { click_on access_type_name }
+ end
+
+ wait_for_ajax
+ expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
+ end
+ end
+end
diff --git a/spec/features/protected_branches_spec.rb b/spec/features/protected_branches_spec.rb
index a0ee6cab7ec..1a3f7b970f6 100644
--- a/spec/features/protected_branches_spec.rb
+++ b/spec/features/protected_branches_spec.rb
@@ -1,4 +1,5 @@
require 'spec_helper'
+Dir["./spec/features/protected_branches/*.rb"].sort.each { |f| require f }
feature 'Projected Branches', feature: true, js: true do
include WaitForAjax
@@ -88,74 +89,6 @@ feature 'Projected Branches', feature: true, js: true do
end
describe "access control" do
- ProtectedBranch::PushAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
- it "allows creating protected branches that #{access_type_name} can push to" do
- visit namespace_project_protected_branches_path(project.namespace, project)
- set_protected_branch_name('master')
- within('.new_protected_branch') do
- allowed_to_push_button = find(".js-allowed-to-push")
-
- unless allowed_to_push_button.text == access_type_name
- allowed_to_push_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
- end
- end
- click_on "Protect"
-
- expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to eq([access_type_id])
- end
-
- it "allows updating protected branches so that #{access_type_name} can push to them" do
- visit namespace_project_protected_branches_path(project.namespace, project)
- set_protected_branch_name('master')
- click_on "Protect"
-
- expect(ProtectedBranch.count).to eq(1)
-
- within(".protected-branches-list") do
- find(".js-allowed-to-push").click
- within('.js-allowed-to-push-container') { click_on access_type_name }
- end
-
- wait_for_ajax
- expect(ProtectedBranch.last.push_access_levels.map(&:access_level)).to include(access_type_id)
- end
- end
-
- ProtectedBranch::MergeAccessLevel.human_access_levels.each do |(access_type_id, access_type_name)|
- it "allows creating protected branches that #{access_type_name} can merge to" do
- visit namespace_project_protected_branches_path(project.namespace, project)
- set_protected_branch_name('master')
- within('.new_protected_branch') do
- allowed_to_merge_button = find(".js-allowed-to-merge")
-
- unless allowed_to_merge_button.text == access_type_name
- allowed_to_merge_button.click
- within(".dropdown.open .dropdown-menu") { click_on access_type_name }
- end
- end
- click_on "Protect"
-
- expect(ProtectedBranch.count).to eq(1)
- expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to eq([access_type_id])
- end
-
- it "allows updating protected branches so that #{access_type_name} can merge to them" do
- visit namespace_project_protected_branches_path(project.namespace, project)
- set_protected_branch_name('master')
- click_on "Protect"
-
- expect(ProtectedBranch.count).to eq(1)
-
- within(".protected-branches-list") do
- find(".js-allowed-to-merge").click
- within('.js-allowed-to-merge-container') { click_on access_type_name }
- end
-
- wait_for_ajax
- expect(ProtectedBranch.last.merge_access_levels.map(&:access_level)).to include(access_type_id)
- end
- end
+ include_examples "protected branches > access control > CE"
end
end
diff --git a/spec/features/security/dashboard_access_spec.rb b/spec/features/security/dashboard_access_spec.rb
index 788581a26cb..40f773956d1 100644
--- a/spec/features/security/dashboard_access_spec.rb
+++ b/spec/features/security/dashboard_access_spec.rb
@@ -43,6 +43,20 @@ describe "Dashboard access", feature: true do
it { is_expected.to be_allowed_for :visitor }
end
+ describe "GET /koding" do
+ subject { koding_path }
+
+ context 'with Koding enabled' do
+ before do
+ stub_application_setting(koding_enabled?: true)
+ end
+
+ it { is_expected.to be_allowed_for :admin }
+ it { is_expected.to be_allowed_for :user }
+ it { is_expected.to be_denied_for :visitor }
+ end
+ end
+
describe "GET /projects/new" do
it { expect(new_project_path).to be_allowed_for :admin }
it { expect(new_project_path).to be_allowed_for :user }
diff --git a/spec/features/todos/todos_sorting_spec.rb b/spec/features/todos/todos_sorting_spec.rb
new file mode 100644
index 00000000000..e74a51acede
--- /dev/null
+++ b/spec/features/todos/todos_sorting_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe "Dashboard > User sorts todos", feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+
+ let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
+ let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
+ let(:label_3) { create(:label, title: 'label_3', project: project, priority: 3) }
+
+ let(:issue_1) { create(:issue, title: 'issue_1', project: project) }
+ let(:issue_2) { create(:issue, title: 'issue_2', project: project) }
+ let(:issue_3) { create(:issue, title: 'issue_3', project: project) }
+ let(:issue_4) { create(:issue, title: 'issue_4', project: project) }
+
+ let!(:merge_request_1) { create(:merge_request, source_project: project, title: "merge_request_1") }
+
+ before do
+ create(:todo, user: user, project: project, target: issue_4, created_at: 5.hours.ago)
+ create(:todo, user: user, project: project, target: issue_2, created_at: 4.hours.ago)
+ create(:todo, user: user, project: project, target: issue_3, created_at: 3.hours.ago)
+ create(:todo, user: user, project: project, target: issue_1, created_at: 2.hours.ago)
+ create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ merge_request_1.labels << label_1
+ issue_3.labels << label_1
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ project.team << [user, :developer]
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it "sorts with oldest created todos first" do
+ click_link "Last created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("merge_request_1")
+ expect(results_list.all('p')[1]).to have_content("issue_1")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+
+ it "sorts with newest created todos first" do
+ click_link "Oldest created"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_4")
+ expect(results_list.all('p')[1]).to have_content("issue_2")
+ expect(results_list.all('p')[2]).to have_content("issue_3")
+ expect(results_list.all('p')[3]).to have_content("issue_1")
+ expect(results_list.all('p')[4]).to have_content("merge_request_1")
+ end
+
+ it "sorts by priority" do
+ click_link "Priority"
+
+ results_list = page.find('.todos-list')
+ expect(results_list.all('p')[0]).to have_content("issue_3")
+ expect(results_list.all('p')[1]).to have_content("merge_request_1")
+ expect(results_list.all('p')[2]).to have_content("issue_1")
+ expect(results_list.all('p')[3]).to have_content("issue_2")
+ expect(results_list.all('p')[4]).to have_content("issue_4")
+ end
+end
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/move_to_project_finder_spec.rb
index 4f3304f7b6d..fdce4e714ff 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/move_to_project_finder_spec.rb
@@ -51,6 +51,28 @@ describe MoveToProjectFinder do
expect(subject.execute(project).to_a).to eq([other_reporter_project])
end
+
+ it 'returns a page of projects ordered by id in descending order' do
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project).to_a).to eq([master_project, developer_project])
+ end
+
+ it 'returns projects after the given offset id' do
+ stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+
+ reporter_project.team << [user, :reporter]
+ developer_project.team << [user, :developer]
+ master_project.team << [user, :master]
+
+ expect(subject.execute(project, search: nil, offset_id: master_project.id).to_a).to eq([developer_project, reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project])
+ expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty
+ end
end
context 'search' do
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
new file mode 100644
index 00000000000..f7e7e733cf7
--- /dev/null
+++ b/spec/finders/todos_finder_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe TodosFinder do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:finder) { described_class }
+
+ before { project.team << [user, :developer] }
+
+ describe '#sort' do
+ context 'by date' do
+ let!(:todo1) { create(:todo, user: user, project: project) }
+ let!(:todo2) { create(:todo, user: user, project: project) }
+ let!(:todo3) { create(:todo, user: user, project: project) }
+
+ it 'sorts with oldest created first' do
+ todos = finder.new(user, { sort: 'id_asc' }).execute
+
+ expect(todos.first).to eq(todo1)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo3)
+ end
+
+ it 'sorts with newest created first' do
+ todos = finder.new(user, { sort: 'id_desc' }).execute
+
+ expect(todos.first).to eq(todo3)
+ expect(todos.second).to eq(todo2)
+ expect(todos.third).to eq(todo1)
+ end
+ end
+
+ it "sorts by priority" do
+ label_1 = create(:label, title: 'label_1', project: project, priority: 1)
+ label_2 = create(:label, title: 'label_2', project: project, priority: 2)
+ label_3 = create(:label, title: 'label_3', project: project, priority: 3)
+
+ issue_1 = create(:issue, title: 'issue_1', project: project)
+ issue_2 = create(:issue, title: 'issue_2', project: project)
+ issue_3 = create(:issue, title: 'issue_3', project: project)
+ issue_4 = create(:issue, title: 'issue_4', project: project)
+ merge_request_1 = create(:merge_request, source_project: project)
+
+ merge_request_1.labels << label_1
+
+ # Covers the case where Todo has more than one label
+ issue_3.labels << label_1
+ issue_3.labels << label_3
+
+ issue_2.labels << label_3
+ issue_1.labels << label_2
+
+ todo_1 = create(:todo, user: user, project: project, target: issue_4)
+ todo_2 = create(:todo, user: user, project: project, target: issue_2)
+ todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago)
+ todo_4 = create(:todo, user: user, project: project, target: issue_1)
+ todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+
+ todos = finder.new(user, { sort: 'priority' }).execute
+
+ expect(todos.first).to eq(todo_3)
+ expect(todos.second).to eq(todo_5)
+ expect(todos.third).to eq(todo_4)
+ expect(todos.fourth).to eq(todo_2)
+ expect(todos.fifth).to eq(todo_1)
+ end
+ end
+ end
+end
diff --git a/spec/fixtures/api/schemas/issue.json b/spec/fixtures/api/schemas/issue.json
index 299e4675d6f..532ebb9640e 100644
--- a/spec/fixtures/api/schemas/issue.json
+++ b/spec/fixtures/api/schemas/issue.json
@@ -10,23 +10,31 @@
"title": { "type": "string" },
"confidential": { "type": "boolean" },
"labels": {
- "type": ["array"],
- "required": [
- "id",
- "color",
- "description",
- "title",
- "priority"
- ],
- "properties": {
- "id": { "type": "integer" },
- "color": {
- "type": "string",
- "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ "type": "array",
+ "items": {
+ "type": "object",
+ "required": [
+ "id",
+ "color",
+ "description",
+ "title",
+ "priority"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "description": { "type": ["string", "null"] },
+ "text_color": {
+ "type": "string",
+ "pattern": "^#[0-9A-Fa-f]{3}{1,2}+$"
+ },
+ "title": { "type": "string" },
+ "priority": { "type": ["integer", "null"] }
},
- "description": { "type": ["string", "null"] },
- "title": { "type": "string" },
- "priority": { "type": ["integer", "null"] }
+ "additionalProperties": false
}
},
"assignee": {
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
new file mode 100644
index 00000000000..2dd2eab0524
--- /dev/null
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe IssuablesHelper do
+ let(:label) { build_stubbed(:label) }
+ let(:label2) { build_stubbed(:label) }
+
+ context 'label tooltip' do
+ it 'returns label text' do
+ expect(issuable_labels_tooltip([label])).to eq(label.title)
+ end
+
+ it 'returns label text' do
+ expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
+ end
+ end
+end
diff --git a/spec/helpers/time_helper_spec.rb b/spec/helpers/time_helper_spec.rb
index bf3ed5c094c..21f35585367 100644
--- a/spec/helpers/time_helper_spec.rb
+++ b/spec/helpers/time_helper_spec.rb
@@ -19,16 +19,16 @@ describe TimeHelper do
describe "#duration_in_numbers" do
it "returns minutes and seconds" do
- duration_in_numbers = {
- [100, 0] => "01:40",
- [121, 0] => "02:01",
- [3721, 0] => "01:02:01",
- [0, 0] => "00:00",
- [nil, Time.now.to_i - 42] => "00:42"
+ durations_and_expectations = {
+ 100 => "01:40",
+ 121 => "02:01",
+ 3721 => "01:02:01",
+ 0 => "00:00",
+ 42 => "00:42"
}
- duration_in_numbers.each do |interval, expectation|
- expect(duration_in_numbers(*interval)).to eq(expectation)
+ durations_and_expectations.each do |duration, expectation|
+ expect(duration_in_numbers(duration)).to eq(expectation)
end
end
end
diff --git a/spec/javascripts/fixtures/gl_dropdown.html.haml b/spec/javascripts/fixtures/gl_dropdown.html.haml
new file mode 100644
index 00000000000..a20390c08ee
--- /dev/null
+++ b/spec/javascripts/fixtures/gl_dropdown.html.haml
@@ -0,0 +1,16 @@
+%div
+ .dropdown.inline
+ %button#js-project-dropdown.dropdown-menu-toggle{type: 'button', data: {toggle: 'dropdown'}}
+ Projects
+ %i.fa.fa-chevron-down.dropdown-toggle-caret.js-projects-dropdown-toggle
+ .dropdown-menu.dropdown-select.dropdown-menu-selectable
+ .dropdown-title
+ %span Go to project
+ %button.dropdown-title-button.dropdown-menu-close{aria: {label: 'Close'}}
+ %i.fa.fa-times.dropdown-menu-close-icon
+ .dropdown-input
+ %input.dropdown-input-field{type: 'search', placeholder: 'Filter results'}
+ %i.fa.fa-search.dropdown-input-search
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/fixtures/issue_sidebar_label.html.haml b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
new file mode 100644
index 00000000000..397bdc85c67
--- /dev/null
+++ b/spec/javascripts/fixtures/issue_sidebar_label.html.haml
@@ -0,0 +1,16 @@
+.block.labels
+ .sidebar-collapsed-icon.js-sidebar-labels-tooltip
+ .title.hide-collapsed
+ %a.edit-link.pull-right{ href: "#" }
+ Edit
+ .selectbox.hide-collapsed{ style: "display: none;" }
+ .dropdown
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect{ type: "button", data: { ability_name: "issue", field_name: "issue[label_names][]", issue_update: "/root/test/issues/2.json", labels: "/root/test/labels.json", project_id: "12", show_any: "true", show_no: "true", toggle: "dropdown" } }
+ %span.dropdown-toggle-text
+ Label
+ %i.fa.fa-chevron-down
+ .dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
+ .dropdown-page-one
+ .dropdown-content
+ .dropdown-loading
+ %i.fa.fa-spinner.fa-spin
diff --git a/spec/javascripts/gl_dropdown_spec.js.es6 b/spec/javascripts/gl_dropdown_spec.js.es6
new file mode 100644
index 00000000000..b529ea6458d
--- /dev/null
+++ b/spec/javascripts/gl_dropdown_spec.js.es6
@@ -0,0 +1,119 @@
+/*= require jquery */
+/*= require gl_dropdown */
+/*= require turbolinks */
+/*= require lib/utils/common_utils */
+/*= require lib/utils/type_utility */
+
+(() => {
+ const NON_SELECTABLE_CLASSES = '.divider, .separator, .dropdown-header, .dropdown-menu-empty-link';
+ const ITEM_SELECTOR = `.dropdown-content li:not(${NON_SELECTABLE_CLASSES})`;
+ const FOCUSED_ITEM_SELECTOR = `${ITEM_SELECTOR} a.is-focused`;
+
+ const ARROW_KEYS = {
+ DOWN: 40,
+ UP: 38,
+ ENTER: 13,
+ ESC: 27
+ };
+
+ let navigateWithKeys = function navigateWithKeys(direction, steps, cb, i) {
+ i = i || 0;
+ if (!i) direction = direction.toUpperCase();
+ $('body').trigger({
+ type: 'keydown',
+ which: ARROW_KEYS[direction],
+ keyCode: ARROW_KEYS[direction]
+ });
+ i++;
+ if (i <= steps) {
+ navigateWithKeys(direction, steps, cb, i);
+ } else {
+ cb();
+ }
+ };
+
+ describe('Dropdown', function describeDropdown() {
+ fixture.preload('gl_dropdown.html');
+ fixture.preload('projects.json');
+
+ beforeEach(() => {
+ fixture.load('gl_dropdown.html');
+ this.dropdownContainerElement = $('.dropdown.inline');
+ this.dropdownMenuElement = $('.dropdown-menu', this.dropdownContainerElement);
+ this.projectsData = fixture.load('projects.json')[0];
+ this.dropdownButtonElement = $('#js-project-dropdown', this.dropdownContainerElement).glDropdown({
+ selectable: true,
+ data: this.projectsData,
+ text: (project) => {
+ (project.name_with_namespace || project.name);
+ },
+ id: (project) => {
+ project.id;
+ }
+ });
+ });
+
+ afterEach(() => {
+ $('body').unbind('keydown');
+ this.dropdownContainerElement.unbind('keyup');
+ });
+
+ it('should open on click', () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ this.dropdownButtonElement.click();
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ });
+
+ describe('that is open', () => {
+ beforeEach(() => {
+ this.dropdownButtonElement.click();
+ });
+
+ it('should select a following item on DOWN keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 1)) + 0);
+ navigateWithKeys('down', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+
+ it('should select a previous item on UP keypress', () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(0);
+ navigateWithKeys('down', (this.projectsData.length - 1), () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ let randomIndex = (Math.floor(Math.random() * (this.projectsData.length - 2)) + 0);
+ navigateWithKeys('up', randomIndex, () => {
+ expect($(FOCUSED_ITEM_SELECTOR, this.dropdownMenuElement).length).toBe(1);
+ expect($(`${ITEM_SELECTOR}:eq(${((this.projectsData.length - 2) - randomIndex)}) a`, this.dropdownMenuElement)).toHaveClass('is-focused');
+ });
+ });
+ });
+
+ it('should click the selected item on ENTER keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open')
+ let randomIndex = Math.floor(Math.random() * (this.projectsData.length - 1)) + 0
+ navigateWithKeys('down', randomIndex, () => {
+ spyOn(Turbolinks, 'visit').and.stub();
+ navigateWithKeys('enter', null, () => {
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ let link = $(`${ITEM_SELECTOR}:eq(${randomIndex}) a`, this.dropdownMenuElement);
+ expect(link).toHaveClass('is-active');
+ let linkedLocation = link.attr('href');
+ if (linkedLocation && linkedLocation !== '#') expect(Turbolinks.visit).toHaveBeenCalledWith(linkedLocation);
+ });
+ });
+ });
+
+ it('should close on ESC keypress', () => {
+ expect(this.dropdownContainerElement).toHaveClass('open');
+ this.dropdownContainerElement.trigger({
+ type: 'keyup',
+ which: ARROW_KEYS.ESC,
+ keyCode: ARROW_KEYS.ESC
+ });
+ expect(this.dropdownContainerElement).not.toHaveClass('open');
+ });
+ });
+ });
+})();
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
new file mode 100644
index 00000000000..840c7b6d015
--- /dev/null
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -0,0 +1,89 @@
+//= require lib/utils/type_utility
+//= require jquery
+//= require bootstrap
+//= require gl_dropdown
+//= require select2
+//= require jquery.nicescroll
+//= require api
+//= require create_label
+//= require issuable_context
+//= require users_select
+//= require labels_select
+
+(() => {
+ let saveLabelCount = 0;
+ describe('Issue dropdown sidebar', () => {
+ fixture.preload('issue_sidebar_label.html');
+
+ beforeEach(() => {
+ fixture.load('issue_sidebar_label.html');
+ new IssuableContext('{"id":1,"name":"Administrator","username":"root"}');
+ new LabelsSelect();
+
+ spyOn(jQuery, 'ajax').and.callFake((req) => {
+ const d = $.Deferred();
+ let LABELS_DATA = []
+
+ if (req.url === '/root/test/labels.json') {
+ for (let i = 0; i < 10; i++) {
+ LABELS_DATA.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ }
+ } else if (req.url === '/root/test/issues/2.json') {
+ let tmp = []
+ for (let i = 0; i < saveLabelCount; i++) {
+ tmp.push({id: i, title: `test ${i}`, color: '#5CB85C'});
+ }
+ LABELS_DATA = {labels: tmp};
+ }
+
+ d.resolve(LABELS_DATA);
+ return d.promise();
+ });
+ });
+
+ it('changes collapsed tooltip when changing labels when less than 5', (done) => {
+ saveLabelCount = 5;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdow-content a').each((i, $link) => {
+ if (i < 5) {
+ $link.get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4');
+ done();
+ }, 0);
+ }, 0);
+ });
+
+ it('changes collapsed tooltip when changing labels when more than 5', (done) => {
+ saveLabelCount = 6;
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.dropdown-content a').length).toBe(10);
+
+ $('.dropdow-content a').each((i, $link) => {
+ if (i < 5) {
+ $link.get(0).click();
+ }
+ });
+
+ $('.edit-link').get(0).click();
+
+ setTimeout(() => {
+ expect($('.sidebar-collapsed-icon').attr('data-original-title')).toBe('test 0, test 1, test 2, test 3, test 4, and 1 more');
+ done();
+ }, 0);
+ }, 0);
+ });
+ });
+})();
+
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 68d64483d67..324f5152780 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -105,13 +105,13 @@
a3 = "a[href='" + mrsAssignedToMeLink + "']";
a4 = "a[href='" + mrsIHaveCreatedLink + "']";
expect(list.find(a1).length).toBe(1);
- expect(list.find(a1).text()).toBe(' Issues assigned to me ');
+ expect(list.find(a1).text()).toBe('Issues assigned to me');
expect(list.find(a2).length).toBe(1);
- expect(list.find(a2).text()).toBe(" Issues I've created ");
+ expect(list.find(a2).text()).toBe("Issues I've created");
expect(list.find(a3).length).toBe(1);
- expect(list.find(a3).text()).toBe(' Merge requests assigned to me ');
+ expect(list.find(a3).text()).toBe('Merge requests assigned to me');
expect(list.find(a4).length).toBe(1);
- return expect(list.find(a4).text()).toBe(" Merge requests I've created ");
+ return expect(list.find(a4).text()).toBe("Merge requests I've created");
};
describe('Search autocomplete dropdown', function() {
diff --git a/spec/lib/extracts_path_spec.rb b/spec/lib/extracts_path_spec.rb
index 36c77206a3f..86d04ecfa36 100644
--- a/spec/lib/extracts_path_spec.rb
+++ b/spec/lib/extracts_path_spec.rb
@@ -30,17 +30,6 @@ describe ExtractsPath, lib: true do
expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
end
- context 'escaped slash character in ref' do
- let(:ref) { 'improve%2Fawesome' }
-
- it 'has no escape sequences in @ref or @logs_path' do
- assign_ref_vars
-
- expect(@ref).to eq('improve/awesome')
- expect(@logs_path).to eq("/#{@project.path_with_namespace}/refs/#{ref}/logs_tree/files/ruby/popen.rb")
- end
- end
-
context 'ref contains %20' do
let(:ref) { 'foo%20bar' }
@@ -52,6 +41,16 @@ describe ExtractsPath, lib: true do
expect(@id).to start_with('foo%20bar/')
end
end
+
+ context 'path contains space' do
+ let(:params) { { path: 'with space', ref: '38008cb17ce1466d8fec2dfa6f6ab8dcfe5cf49e' } }
+
+ it 'is not converted to %20 in @path' do
+ assign_ref_vars
+
+ expect(@path).to eq(params[:path])
+ end
+ end
end
describe '#extract_ref' do
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
index 1ff49602486..ab0cce6e091 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -44,45 +44,49 @@ describe Gitlab::Badge::Coverage::Report do
end
end
- context 'pipeline exists' do
- let!(:pipeline) do
- create(:ci_pipeline, project: project,
- sha: project.commit.id,
- ref: 'master')
- end
+ context 'when latest successful pipeline exists' do
+ before do
+ create_pipeline do |pipeline|
+ create(:ci_build, :success, pipeline: pipeline, name: 'first', coverage: 40)
+ create(:ci_build, :success, pipeline: pipeline, coverage: 60)
+ end
- context 'builds exist' do
- before do
- create(:ci_build, name: 'first', pipeline: pipeline, coverage: 40)
- create(:ci_build, pipeline: pipeline, coverage: 60)
+ create_pipeline do |pipeline|
+ create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
end
+ end
- context 'particular job specified' do
- let(:job_name) { 'first' }
+ context 'when particular job specified' do
+ let(:job_name) { 'first' }
- it 'returns coverage for the particular job' do
- expect(badge.status).to eq 40
- end
+ it 'returns coverage for the particular job' do
+ expect(badge.status).to eq 40
end
+ end
- context 'particular job not specified' do
- let(:job_name) { '' }
+ context 'when particular job not specified' do
+ let(:job_name) { '' }
+
+ it 'returns arithemetic mean for the pipeline' do
+ expect(badge.status).to eq 50
+ end
+ end
+ end
- it 'returns arithemetic mean for the pipeline' do
- expect(badge.status).to eq 50
- end
+ context 'when only failed pipeline exists' do
+ before do
+ create_pipeline do |pipeline|
+ create(:ci_build, :failed, pipeline: pipeline, coverage: 10)
end
end
- context 'builds do not exist' do
- it_behaves_like 'unknown coverage report'
+ it_behaves_like 'unknown coverage report'
- context 'particular job specified' do
- let(:job_name) { 'nonexistent' }
+ context 'particular job specified' do
+ let(:job_name) { 'nonexistent' }
- it 'retruns nil' do
- expect(badge.status).to be_nil
- end
+ it 'retruns nil' do
+ expect(badge.status).to be_nil
end
end
end
@@ -90,4 +94,13 @@ describe Gitlab::Badge::Coverage::Report do
context 'pipeline does not exist' do
it_behaves_like 'unknown coverage report'
end
+
+ def create_pipeline
+ opts = { project: project, sha: project.commit.id, ref: 'master' }
+
+ create(:ci_pipeline, opts).tap do |pipeline|
+ yield pipeline
+ pipeline.build_updated
+ end
+ end
end
diff --git a/spec/lib/gitlab/diff/position_spec.rb b/spec/lib/gitlab/diff/position_spec.rb
index 10537bea008..6e8fff6f516 100644
--- a/spec/lib/gitlab/diff/position_spec.rb
+++ b/spec/lib/gitlab/diff/position_spec.rb
@@ -339,6 +339,48 @@ describe Gitlab::Diff::Position, lib: true do
end
end
+ describe "position for a file in the initial commit" do
+ let(:commit) { project.commit("1a0b36b3cdad1d2ee32457c102a8c0b7056fa863") }
+
+ subject do
+ described_class.new(
+ old_path: "README.md",
+ new_path: "README.md",
+ old_line: nil,
+ new_line: 1,
+ diff_refs: commit.diff_refs
+ )
+ end
+
+ describe "#diff_file" do
+ it "returns the correct diff file" do
+ diff_file = subject.diff_file(project.repository)
+
+ expect(diff_file.new_file).to be true
+ expect(diff_file.new_path).to eq(subject.new_path)
+ expect(diff_file.diff_refs).to eq(subject.diff_refs)
+ end
+ end
+
+ describe "#diff_line" do
+ it "returns the correct diff line" do
+ diff_line = subject.diff_line(project.repository)
+
+ expect(diff_line.added?).to be true
+ expect(diff_line.new_line).to eq(subject.new_line)
+ expect(diff_line.text).to eq("+testme")
+ end
+ end
+
+ describe "#line_code" do
+ it "returns the correct line code" do
+ line_code = Gitlab::Diff::LineCode.generate(subject.file_path, subject.new_line, 0)
+
+ expect(subject.line_code(project.repository)).to eq(line_code)
+ end
+ end
+ end
+
describe "#to_json" do
let(:hash) do
{
diff --git a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
index e1153154778..a5cc7b02936 100644
--- a/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_issue_handler_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require_relative '../email_shared_blocks'
-describe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
+xdescribe Gitlab::Email::Handler::CreateIssueHandler, lib: true do
include_context :email_shared_context
it_behaves_like :email_shared_examples
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index fa241867858..eae9c060c38 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -493,7 +493,12 @@ describe Notify do
end
def invite_to_project(project:, email:, inviter:)
- ProjectMember.add_user(project.project_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ project.project_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
project.project_members.invite.last
end
@@ -740,7 +745,12 @@ describe Notify do
end
def invite_to_group(group:, email:, inviter:)
- GroupMember.add_user(group.group_members, 'toto@example.com', Gitlab::Access::DEVELOPER, inviter)
+ Member.add_user(
+ group.group_members,
+ 'toto@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: inviter
+ )
group.group_members.invite.last
end
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index 853f6943cef..aa3b2bbf471 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -171,6 +171,70 @@ describe Ability, lib: true do
end
end
+ shared_examples_for ".project_abilities" do |enable_request_store|
+ before do
+ RequestStore.begin! if enable_request_store
+ end
+
+ after do
+ if enable_request_store
+ RequestStore.end!
+ RequestStore.clear!
+ end
+ end
+
+ describe '.project_abilities' do
+ let!(:project) { create(:empty_project, :public) }
+ let!(:user) { create(:user) }
+
+ it 'returns permissions for admin user' do
+ admin = create(:admin)
+
+ results = described_class.project_abilities(admin, project)
+
+ expect(results.count).to eq(68)
+ end
+
+ it 'returns permissions for an owner' do
+ results = described_class.project_abilities(project.owner, project)
+
+ expect(results.count).to eq(68)
+ end
+
+ it 'returns permissions for a master' do
+ project.team << [user, :master]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(60)
+ end
+
+ it 'returns permissions for a developer' do
+ project.team << [user, :developer]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(44)
+ end
+
+ it 'returns permissions for a guest' do
+ project.team << [user, :guest]
+
+ results = described_class.project_abilities(user, project)
+
+ expect(results.count).to eq(21)
+ end
+ end
+ end
+
+ describe '.project_abilities with RequestStore' do
+ it_behaves_like ".project_abilities", true
+ end
+
+ describe '.project_abilities without RequestStore' do
+ it_behaves_like ".project_abilities", false
+ end
+
describe '.issues_readable_by_user' do
context 'with an admin user' do
it 'returns all given issues' do
diff --git a/spec/models/broadcast_message_spec.rb b/spec/models/broadcast_message_spec.rb
index 72688137f08..02d6263094a 100644
--- a/spec/models/broadcast_message_spec.rb
+++ b/spec/models/broadcast_message_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
describe BroadcastMessage, models: true do
- include ActiveSupport::Testing::TimeHelpers
-
subject { create(:broadcast_message) }
it { is_expected.to be_valid }
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 8137e9f8f71..721b20e0cb2 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -124,17 +124,21 @@ describe Ci::Pipeline, models: true do
describe 'state machine' do
let(:current) { Time.now.change(usec: 0) }
- let(:build) { create :ci_build, name: 'build1', pipeline: pipeline, started_at: current - 60, finished_at: current }
- let(:build2) { create :ci_build, name: 'build2', pipeline: pipeline, started_at: current - 60, finished_at: current }
+ let(:build) { create :ci_build, name: 'build1', pipeline: pipeline }
describe '#duration' do
before do
- build.skip
- build2.skip
+ travel_to(current - 120) do
+ pipeline.run
+ end
+
+ travel_to(current) do
+ pipeline.succeed
+ end
end
it 'matches sum of builds duration' do
- expect(pipeline.reload.duration).to eq(build.duration + build2.duration)
+ expect(pipeline.reload.duration).to eq(120)
end
end
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 2277f4e13bf..fef90d9b5cb 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -65,11 +65,21 @@ describe Member, models: true do
@master_user = create(:user).tap { |u| project.team << [u, :master] }
@master = project.members.find_by(user_id: @master_user.id)
- ProjectMember.add_user(project.members, 'toto1@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ Member.add_user(
+ project.members,
+ 'toto1@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @master_user
+ )
@invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
accepted_invite_user = build(:user)
- ProjectMember.add_user(project.members, 'toto2@example.com', Gitlab::Access::DEVELOPER, @master_user)
+ Member.add_user(
+ project.members,
+ 'toto2@example.com',
+ Gitlab::Access::DEVELOPER,
+ current_user: @master_user
+ )
@accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
diff --git a/spec/models/network/graph_spec.rb b/spec/models/network/graph_spec.rb
new file mode 100644
index 00000000000..b76513d2a3c
--- /dev/null
+++ b/spec/models/network/graph_spec.rb
@@ -0,0 +1,12 @@
+require 'spec_helper'
+
+describe Network::Graph, models: true do
+ let(:project) { create(:project) }
+ let!(:note_on_commit) { create(:note_on_commit, project: project) }
+
+ it '#initialize' do
+ graph = described_class.new(project, 'refs/heads/master', project.repository.commit, nil)
+
+ expect(graph.notes).to eq( { note_on_commit.commit_id => 1 } )
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index d1f3a815290..9a3660012f9 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -247,7 +247,7 @@ describe Project, models: true do
end
end
- describe "#new_issue_address" do
+ xdescribe "#new_issue_address" do
let(:project) { create(:empty_project, path: "somewhere") }
let(:user) { create(:user) }
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f7dbfd712cc..1fea50ad42c 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -719,6 +719,14 @@ describe Repository, models: true do
expect(merge_commit).to be_present
expect(repository.blob_at(merge_commit.id, 'files/ruby/feature.rb')).to be_present
end
+
+ it 'sets the `in_progress_merge_commit_sha` flag for the given merge request' do
+ merge_request = create(:merge_request, source_branch: 'feature', target_branch: 'master', source_project: project)
+ merge_commit_id = repository.merge(user, merge_request, commit_options)
+ repository.commit(merge_commit_id)
+
+ expect(merge_request.in_progress_merge_commit_sha).to eq(merge_commit_id)
+ end
end
describe '#revert' do
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index a56ee30f7b1..1e365bf353a 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -122,12 +122,13 @@ describe API::Members, api: true do
it 'creates a new member' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/members", master),
- user_id: stranger.id, access_level: Member::DEVELOPER
+ user_id: stranger.id, access_level: Member::DEVELOPER, expires_at: '2016-08-05'
expect(response).to have_http_status(201)
end.to change { source.members.count }.by(1)
expect(json_response['id']).to eq(stranger.id)
expect(json_response['access_level']).to eq(Member::DEVELOPER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
end
end
@@ -183,11 +184,12 @@ describe API::Members, api: true do
context 'when authenticated as a master/owner' do
it 'updates the member' do
put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", master),
- access_level: Member::MASTER
+ access_level: Member::MASTER, expires_at: '2016-08-05'
expect(response).to have_http_status(200)
expect(json_response['id']).to eq(developer.id)
expect(json_response['access_level']).to eq(Member::MASTER)
+ expect(json_response['expires_at']).to eq('2016-08-05')
end
end
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 1d4df9197f6..d65648dd0b2 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -116,12 +116,19 @@ describe HelpController, "routing" do
expect(get(path)).to route_to('help#show',
path: 'workflow/protected_branches/protected_branches1',
format: 'png')
-
+
path = '/help/ui'
expect(get(path)).to route_to('help#ui')
end
end
+# koding GET /koding(.:format) koding#index
+describe KodingController, "routing" do
+ it "to #index" do
+ expect(get("/koding")).to route_to('koding#index')
+ end
+end
+
# profile_account GET /profile/account(.:format) profile#account
# profile_history GET /profile/history(.:format) profile#history
# profile_password PUT /profile/password(.:format) profile#password_update
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index ad8c2485888..8326e5cd313 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -3,8 +3,6 @@ require 'spec_helper'
describe Ci::ProcessPipelineService, services: true do
let(:pipeline) { create(:ci_pipeline, ref: 'master') }
let(:user) { create(:user) }
- let(:all_builds) { pipeline.builds }
- let(:builds) { all_builds.where.not(status: [:created, :skipped]) }
let(:config) { nil }
before do
@@ -12,6 +10,14 @@ describe Ci::ProcessPipelineService, services: true do
end
describe '#execute' do
+ def all_builds
+ pipeline.builds
+ end
+
+ def builds
+ all_builds.where.not(status: [:created, :skipped])
+ end
+
def create_builds
described_class.new(pipeline.project, user).execute(pipeline)
end
@@ -48,7 +54,7 @@ describe Ci::ProcessPipelineService, services: true do
it 'does not process pipeline if existing stage is running' do
expect(create_builds).to be_truthy
expect(builds.pending.count).to eq(2)
-
+
expect(create_builds).to be_falsey
expect(builds.pending.count).to eq(2)
end
@@ -224,6 +230,40 @@ describe Ci::ProcessPipelineService, services: true do
end
end
+ context 'when failed build in the middle stage is retried' do
+ context 'when failed build is the only unsuccessful build in the stage' do
+ before do
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:1', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'build:2', stage_idx: 0)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:1', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'test:2', stage_idx: 1)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:1', stage_idx: 2)
+ create(:ci_build, :created, pipeline: pipeline, name: 'deploy:2', stage_idx: 2)
+ end
+
+ it 'does trigger builds in the next stage' do
+ expect(create_builds).to be_truthy
+ expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
+
+ pipeline.builds.running_or_pending.each(&:success)
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ pipeline.builds.find_by(name: 'test:1').success
+ pipeline.builds.find_by(name: 'test:2').drop
+
+ expect(builds.pluck(:name))
+ .to contain_exactly('build:1', 'build:2', 'test:1', 'test:2')
+
+ Ci::Build.retry(pipeline.builds.find_by(name: 'test:2')).success
+
+ expect(builds.pluck(:name)).to contain_exactly(
+ 'build:1', 'build:2', 'test:1', 'test:2', 'test:2', 'deploy:1', 'deploy:2')
+ end
+ end
+ end
+
context 'creates a builds from .gitlab-ci.yml' do
let(:config) do
YAML.dump({
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 18da3b1b453..f81a58899fd 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1113,6 +1113,46 @@ describe NotificationService, services: true do
end
end
+ describe 'GroupMember' do
+ describe '#decline_group_invite' do
+ let(:creator) { create(:user) }
+ let(:group) { create(:group) }
+ let(:member) { create(:user) }
+
+ before(:each) do
+ group.add_owner(creator)
+ group.add_developer(member, creator)
+ end
+
+ it do
+ group_member = group.members.first
+
+ expect do
+ notification.decline_group_invite(group_member)
+ end.to change { ActionMailer::Base.deliveries.size }.by(1)
+ end
+ end
+ end
+
+ describe 'ProjectMember' do
+ describe '#decline_group_invite' do
+ let(:project) { create(:project) }
+ let(:member) { create(:user) }
+
+ before(:each) do
+ project.team << [member, :developer, project.owner]
+ end
+
+ it do
+ project_member = project.members.first
+
+ expect do
+ notification.decline_project_invite(project_member)
+ end.to change { ActionMailer::Base.deliveries.size }.by(1)
+ end
+ end
+ end
+
def build_team(project)
@u_watcher = create_global_setting_for(create(:user), :watch)
@u_participating = create_global_setting_for(create(:user), :participating)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 2e2aa7c4fc0..c144cd85487 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -33,6 +33,7 @@ RSpec.configure do |config|
config.include EmailHelpers
config.include TestEnv
config.include ActiveJob::TestHelper
+ config.include ActiveSupport::Testing::TimeHelpers
config.include StubGitlabCalls
config.include StubGitlabData
diff --git a/spec/workers/emails_on_push_worker_spec.rb b/spec/workers/emails_on_push_worker_spec.rb
index eecc32875a5..7ca2c29da1c 100644
--- a/spec/workers/emails_on_push_worker_spec.rb
+++ b/spec/workers/emails_on_push_worker_spec.rb
@@ -2,19 +2,19 @@ require 'spec_helper'
describe EmailsOnPushWorker do
include RepoHelpers
+ include EmailSpec::Matchers
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
let(:recipients) { user.email }
let(:perform) { subject.perform(project.id, recipients, data.stringify_keys) }
+ let(:email) { ActionMailer::Base.deliveries.last }
subject { EmailsOnPushWorker.new }
describe "#perform" do
context "when push is a new branch" do
- let(:email) { ActionMailer::Base.deliveries.last }
-
before do
data_new_branch = data.stringify_keys.merge("before" => Gitlab::Git::BLANK_SHA)
@@ -31,8 +31,6 @@ describe EmailsOnPushWorker do
end
context "when push is a deleted branch" do
- let(:email) { ActionMailer::Base.deliveries.last }
-
before do
data_deleted_branch = data.stringify_keys.merge("after" => Gitlab::Git::BLANK_SHA)
@@ -48,15 +46,40 @@ describe EmailsOnPushWorker do
end
end
- context "when there are no errors in sending" do
- let(:email) { ActionMailer::Base.deliveries.last }
+ context "when push is a force push to delete commits" do
+ before do
+ data_force_push = data.stringify_keys.merge(
+ "after" => data[:before],
+ "before" => data[:after]
+ )
+
+ subject.perform(project.id, recipients, data_force_push)
+ end
+
+ it "sends a mail with the correct subject" do
+ expect(email.subject).to include('Change some files')
+ end
+ it "mentions force pushing in the body" do
+ expect(email).to have_body_text("force push")
+ end
+
+ it "sends the mail to the correct recipient" do
+ expect(email.to).to eq([user.email])
+ end
+ end
+
+ context "when there are no errors in sending" do
before { perform }
it "sends a mail with the correct subject" do
expect(email.subject).to include('Change some files')
end
+ it "does not mention force pushing in the body" do
+ expect(email).not_to have_body_text("force push")
+ end
+
it "sends the mail to the correct recipient" do
expect(email.to).to eq([user.email])
end
@@ -66,6 +89,7 @@ describe EmailsOnPushWorker do
before do
ActionMailer::Base.deliveries.clear
allow(Notify).to receive(:repository_push_email).and_raise(Net::SMTPFatalError)
+ allow(subject).to receive_message_chain(:logger, :info)
perform
end
diff --git a/spec/workers/remove_expired_group_links_worker_spec.rb b/spec/workers/remove_expired_group_links_worker_spec.rb
new file mode 100644
index 00000000000..689bc3d27b4
--- /dev/null
+++ b/spec/workers/remove_expired_group_links_worker_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe RemoveExpiredGroupLinksWorker do
+ describe '#perform' do
+ let!(:expired_project_group_link) { create(:project_group_link, expires_at: 1.hour.ago) }
+ let!(:project_group_link_expiring_in_future) { create(:project_group_link, expires_at: 10.days.from_now) }
+ let!(:non_expiring_project_group_link) { create(:project_group_link, expires_at: nil) }
+
+ it 'removes expired group links' do
+ expect { subject.perform }.to change { ProjectGroupLink.count }.by(-1)
+ expect(ProjectGroupLink.find_by(id: expired_project_group_link.id)).to be_nil
+ end
+
+ it 'leaves group links that expire in the future' do
+ subject.perform
+ expect(project_group_link_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves group links that do not expire at all' do
+ subject.perform
+ expect(non_expiring_project_group_link.reload).to be_present
+ end
+ end
+end
diff --git a/spec/workers/remove_expired_members_worker_spec.rb b/spec/workers/remove_expired_members_worker_spec.rb
new file mode 100644
index 00000000000..402aa1e714e
--- /dev/null
+++ b/spec/workers/remove_expired_members_worker_spec.rb
@@ -0,0 +1,58 @@
+require 'spec_helper'
+
+describe RemoveExpiredMembersWorker do
+ let(:worker) { RemoveExpiredMembersWorker.new }
+
+ describe '#perform' do
+ context 'project members' do
+ let!(:expired_project_member) { create(:project_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:project_member_expiring_in_future) { create(:project_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_project_member) { create(:project_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_project_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(project_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_project_member.reload).to be_present
+ end
+ end
+
+ context 'group members' do
+ let!(:expired_group_member) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::DEVELOPER) }
+ let!(:group_member_expiring_in_future) { create(:group_member, expires_at: 10.days.from_now, access_level: GroupMember::DEVELOPER) }
+ let!(:non_expiring_group_member) { create(:group_member, expires_at: nil, access_level: GroupMember::DEVELOPER) }
+
+ it 'removes expired members' do
+ expect { worker.perform }.to change { Member.count }.by(-1)
+ expect(Member.find_by(id: expired_group_member.id)).to be_nil
+ end
+
+ it 'leaves members that expire in the future' do
+ worker.perform
+ expect(group_member_expiring_in_future.reload).to be_present
+ end
+
+ it 'leaves members that do not expire at all' do
+ worker.perform
+ expect(non_expiring_group_member.reload).to be_present
+ end
+ end
+
+ context 'when the last group owner expires' do
+ let!(:expired_group_owner) { create(:group_member, expires_at: 1.hour.ago, access_level: GroupMember::OWNER) }
+
+ it 'does not delete the owner' do
+ worker.perform
+ expect(expired_group_owner.reload).to be_present
+ end
+ end
+ end
+end
diff --git a/vendor/assets/javascripts/Chart.js b/vendor/assets/javascripts/Chart.js
index c264262ba73..c264262ba73 100755..100644
--- a/vendor/assets/javascripts/Chart.js
+++ b/vendor/assets/javascripts/Chart.js
diff --git a/vendor/assets/javascripts/autosize.js b/vendor/assets/javascripts/autosize.js
index cfa49e72c50..cfa49e72c50 100755..100644
--- a/vendor/assets/javascripts/autosize.js
+++ b/vendor/assets/javascripts/autosize.js
diff --git a/vendor/assets/javascripts/jquery.scrollTo.js b/vendor/assets/javascripts/jquery.scrollTo.js
index 7ba17766b70..7ba17766b70 100755..100644
--- a/vendor/assets/javascripts/jquery.scrollTo.js
+++ b/vendor/assets/javascripts/jquery.scrollTo.js