Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJames Lopez <james@jameslopez.es>2016-04-21 09:37:38 +0300
committerJames Lopez <james@jameslopez.es>2016-04-21 09:37:38 +0300
commit07f8ffbdaf2878fb97ae41a3e88ae5358e82dab7 (patch)
treeac822c75ae2448458dbee52edb611497e3c46d53
parent976593fa7fd657437c68ade72f2061260318e7a9 (diff)
parent2ade37e2534108c72d28605cb131dacf771d27d3 (diff)
Merge branch 'master' of gitlab.com:gitlab-org/gitlab-ce into fix/label-filters
# Conflicts: # spec/features/issues/filter_by_labels_spec.rb
-rw-r--r--.rubocop.yml2
-rw-r--r--CHANGELOG40
-rw-r--r--CONTRIBUTING.md1
-rw-r--r--Gemfile3
-rw-r--r--Gemfile.lock3
-rw-r--r--VERSION2
-rw-r--r--app/assets/javascripts/api.js.coffee11
-rw-r--r--app/assets/javascripts/application.js.coffee1
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js.coffee30
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee67
-rw-r--r--app/assets/javascripts/blob/new_blob.js.coffee20
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee3
-rw-r--r--app/assets/javascripts/due_date_select.js.coffee64
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee11
-rw-r--r--app/assets/javascripts/issuable.js.coffee84
-rw-r--r--app/assets/javascripts/issuable_context.js.coffee38
-rw-r--r--app/assets/javascripts/issues.js.coffee51
-rw-r--r--app/assets/javascripts/labels_select.js.coffee73
-rw-r--r--app/assets/javascripts/lib/animate.js.coffee34
-rw-r--r--app/assets/javascripts/lib/url_utility.js.coffee16
-rw-r--r--app/assets/javascripts/merge_requests.js.coffee35
-rw-r--r--app/assets/javascripts/milestone_select.js.coffee6
-rw-r--r--app/assets/javascripts/raven_config.js.coffee44
-rw-r--r--app/assets/javascripts/todos.js.coffee43
-rw-r--r--app/assets/javascripts/users_select.js.coffee2
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss130
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss9
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/detail_page.scss6
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/editor.scss9
-rw-r--r--app/assets/stylesheets/pages/help.scss2
-rw-r--r--app/assets/stylesheets/pages/issuable.scss5
-rw-r--r--app/assets/stylesheets/pages/note_form.scss13
-rw-r--r--app/assets/stylesheets/pages/notes.scss21
-rw-r--r--app/controllers/admin/application_settings_controller.rb2
-rw-r--r--app/controllers/admin/hooks_controller.rb2
-rw-r--r--app/controllers/projects/builds_controller.rb10
-rw-r--r--app/controllers/projects/commit_controller.rb30
-rw-r--r--app/controllers/projects/group_links_controller.rb10
-rw-r--r--app/controllers/projects/issues_controller.rb12
-rw-r--r--app/controllers/projects/merge_requests_controller.rb5
-rw-r--r--app/controllers/projects/services_controller.rb2
-rw-r--r--app/controllers/projects/wikis_controller.rb17
-rw-r--r--app/finders/issuable_finder.rb41
-rw-r--r--app/helpers/application_helper.rb9
-rw-r--r--app/helpers/application_settings_helper.rb4
-rw-r--r--app/helpers/blob_helper.rb11
-rw-r--r--app/helpers/commits_helper.rb31
-rw-r--r--app/helpers/import_helper.rb18
-rw-r--r--app/helpers/issuables_helper.rb19
-rw-r--r--app/helpers/issues_helper.rb14
-rw-r--r--app/helpers/projects_helper.rb59
-rw-r--r--app/helpers/selects_helper.rb25
-rw-r--r--app/helpers/sorting_helper.rb18
-rw-r--r--app/helpers/tree_helper.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/models/ci/build.rb23
-rw-r--r--app/models/commit.rb12
-rw-r--r--app/models/group.rb1
-rw-r--r--app/models/hooks/project_hook.rb3
-rw-r--r--app/models/hooks/system_hook.rb3
-rw-r--r--app/models/hooks/web_hook.rb3
-rw-r--r--app/models/issue.rb46
-rw-r--r--app/models/label.rb4
-rw-r--r--app/models/merge_request.rb7
-rw-r--r--app/models/merge_request_diff.rb2
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/project.rb22
-rw-r--r--app/models/project_import_data.rb1
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/slack_service.rb5
-rw-r--r--app/models/project_services/slack_service/merge_message.rb2
-rw-r--r--app/models/project_services/slack_service/note_message.rb2
-rw-r--r--app/models/project_services/slack_service/wiki_page_message.rb53
-rw-r--r--app/models/repository.rb86
-rw-r--r--app/models/service.rb4
-rw-r--r--app/models/user.rb1
-rw-r--r--app/models/wiki_page.rb4
-rw-r--r--app/services/commits/change_service.rb47
-rw-r--r--app/services/commits/cherry_pick_service.rb19
-rw-r--r--app/services/commits/revert_service.rb46
-rw-r--r--app/services/git_push_service.rb6
-rw-r--r--app/services/git_tag_push_service.rb29
-rw-r--r--app/services/system_hooks_service.rb12
-rw-r--r--app/services/wiki_pages/base_service.rb27
-rw-r--r--app/services/wiki_pages/create_service.rb13
-rw-r--r--app/services/wiki_pages/update_service.rb11
-rw-r--r--app/views/admin/application_settings/_form.html.haml17
-rw-r--r--app/views/admin/hooks/index.html.haml36
-rw-r--r--app/views/admin/users/_form.html.haml6
-rw-r--r--app/views/dashboard/todos/index.html.haml1
-rw-r--r--app/views/import/github/status.html.haml4
-rw-r--r--app/views/notify/closed_merge_request_email.html.haml2
-rw-r--r--app/views/notify/closed_merge_request_email.text.haml2
-rw-r--r--app/views/notify/merge_request_status_email.html.haml2
-rw-r--r--app/views/notify/merge_request_status_email.text.haml2
-rw-r--r--app/views/notify/merged_merge_request_email.html.haml2
-rw-r--r--app/views/notify/merged_merge_request_email.text.haml2
-rw-r--r--app/views/notify/new_merge_request_email.text.erb2
-rw-r--r--app/views/notify/note_merge_request_email.text.erb2
-rw-r--r--app/views/projects/_activity.html.haml5
-rw-r--r--app/views/projects/_readme.html.haml4
-rw-r--r--app/views/projects/blob/_editor.html.haml6
-rw-r--r--app/views/projects/blob/new.html.haml2
-rw-r--r--app/views/projects/builds/show.html.haml7
-rw-r--r--app/views/projects/commit/_change.html.haml (renamed from app/views/projects/commit/_revert.html.haml)20
-rw-r--r--app/views/projects/commit/_commit_box.html.haml1
-rw-r--r--app/views/projects/commit/show.html.haml3
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/empty.html.haml6
-rw-r--r--app/views/projects/hooks/index.html.haml11
-rw-r--r--app/views/projects/issues/_issue.html.haml5
-rw-r--r--app/views/projects/merge_requests/_show.html.haml6
-rw-r--r--app/views/projects/merge_requests/edit.html.haml4
-rw-r--r--app/views/projects/merge_requests/invalid.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml4
-rw-r--r--app/views/projects/merge_requests/widget/_merged.html.haml5
-rw-r--r--app/views/projects/merge_requests/widget/_merged_buttons.haml11
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml7
-rw-r--r--app/views/projects/show.html.haml12
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/_labels_row.html.haml3
-rw-r--r--app/views/shared/_service_settings.html.haml8
-rw-r--r--app/views/shared/_sort_dropdown.html.haml5
-rw-r--r--app/views/shared/issuable/_filter.html.haml8
-rw-r--r--app/views/shared/issuable/_form.html.haml20
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml8
-rw-r--r--app/views/shared/issuable/_nav.html.haml10
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml42
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/users/show.html.haml2
-rw-r--r--app/workers/post_receive.rb4
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20160227120001_add_event_field_for_web_hook.rb5
-rw-r--r--db/migrate/20160227120047_add_event_to_services.rb5
-rw-r--r--db/migrate/20160310124959_add_due_date_to_issues.rb6
-rw-r--r--db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb5
-rw-r--r--db/migrate/20160419120017_add_metrics_packet_size.rb5
-rw-r--r--db/schema.rb44
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/api/licenses.md147
-rw-r--r--doc/ci/api/builds.md108
-rw-r--r--doc/ci/runners/README.md6
-rw-r--r--doc/ci/yaml/README.md33
-rw-r--r--doc/install/requirements.md20
-rw-r--r--doc/intro/README.md1
-rw-r--r--doc/system_hooks/system_hooks.md113
-rw-r--r--doc/web_hooks/web_hooks.md4
-rw-r--r--doc/workflow/README.md1
-rw-r--r--doc/workflow/cherry_pick_changes.md52
-rw-r--r--doc/workflow/img/cherry_pick_changes_commit.pngbin0 -> 353067 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_commit_modal.pngbin0 -> 312659 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_mr.pngbin0 -> 252085 bytes
-rw-r--r--doc/workflow/img/cherry_pick_changes_mr_modal.pngbin0 -> 225569 bytes
-rw-r--r--doc/workflow/lfs/manage_large_binaries_with_git_lfs.md2
-rw-r--r--features/project/issues/filter_labels.feature1
-rw-r--r--features/project/source/browse_files.feature13
-rw-r--r--features/steps/dashboard/todos.rb10
-rw-r--r--features/steps/project/commits/user_lookup.rb3
-rw-r--r--features/steps/project/forked_merge_requests.rb2
-rw-r--r--features/steps/project/issues/filter_labels.rb4
-rw-r--r--features/steps/project/source/browse_files.rb4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities.rb12
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/licenses.rb58
-rw-r--r--lib/api/tags.rb2
-rw-r--r--lib/ci/api/api.rb2
-rw-r--r--lib/ci/api/builds.rb33
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb75
-rw-r--r--lib/file_size_validator.rb8
-rw-r--r--lib/gitlab/gon_helper.rb1
-rw-r--r--lib/gitlab/metrics.rb16
-rw-r--r--lib/gitlab/push_data_builder.rb3
-rw-r--r--lib/gitlab/url_builder.rb6
-rwxr-xr-xscripts/prepare_build.sh2
-rw-r--r--spec/controllers/commit_controller_spec.rb51
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb50
-rw-r--r--spec/factories/project_wikis.rb7
-rw-r--r--spec/factories/wiki_pages.rb9
-rw-r--r--spec/features/builds_spec.rb30
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb13
-rw-r--r--spec/features/issues/filter_issues_spec.rb9
-rw-r--r--spec/features/issues/update_issues_spec.rb2
-rw-r--r--spec/features/issues_spec.rb98
-rw-r--r--spec/features/merge_requests/cherry_pick_spec.rb44
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb6
-rw-r--r--spec/features/project/commits/cherry_pick_spec.rb67
-rw-r--r--spec/features/projects/files/project_owner_creates_license_file_spec.rb61
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb39
-rw-r--r--spec/features/runners_spec.rb16
-rw-r--r--spec/features/todos/todos_spec.rb79
-rw-r--r--spec/finders/issues_finder_spec.rb16
-rw-r--r--spec/helpers/commits_helper_spec.rb29
-rw-r--r--spec/helpers/import_helper_spec.rb25
-rw-r--r--spec/lib/ci/gitlab_ci_yaml_processor_spec.rb102
-rw-r--r--spec/lib/gitlab/metrics_spec.rb24
-rw-r--r--spec/lib/gitlab/push_data_builder_spec.rb15
-rw-r--r--spec/lib/gitlab/url_builder_spec.rb9
-rw-r--r--spec/mailers/notify_spec.rb12
-rw-r--r--spec/models/issue_spec.rb41
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb4
-rw-r--r--spec/models/project_services/slack_service/merge_message_spec.rb4
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb2
-rw-r--r--spec/models/project_services/slack_service/wiki_page_message_spec.rb74
-rw-r--r--spec/models/project_services/slack_service_spec.rb17
-rw-r--r--spec/models/repository_spec.rb100
-rw-r--r--spec/requests/api/licenses_spec.rb136
-rw-r--r--spec/requests/api/tags_spec.rb2
-rw-r--r--spec/requests/ci/api/builds_spec.rb46
-rw-r--r--spec/services/git_tag_push_service_spec.rb24
-rw-r--r--spec/support/project_hook_data_shared_example.rb17
-rw-r--r--spec/support/repo_helpers.rb2
-rw-r--r--vendor/assets/javascripts/raven.js2435
218 files changed, 5866 insertions, 725 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 2fda0b03119..83ed6c38678 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -728,7 +728,7 @@ Metrics/ParameterLists:
# A complexity metric geared towards measuring complexity for a human reader.
Metrics/PerceivedComplexity:
Enabled: true
- Max: 17
+ Max: 18
#################### Lint ################################
diff --git a/CHANGELOG b/CHANGELOG
index 1a4b367e474..d4b8a509261 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,6 +1,9 @@
Please view this file on the master branch, on stable branches it's out of date.
+v 8.8.0 (unreleased)
+
v 8.7.0 (unreleased)
+ - The number of InfluxDB points stored per UDP packet can now be configured
- Transactions for /internal/allowed now have an "action" tag set
- Method instrumentation now uses Module#prepend instead of aliasing methods
- Repository.clean_old_archives is now instrumented
@@ -11,22 +14,26 @@ v 8.7.0 (unreleased)
- Developers can now add custom tags to transactions
- Loading of an issue's referenced merge requests and related branches is now done asynchronously
- Enable gzip for assets, makes the page size significantly smaller. !3544 / !3632 (Connor Shea)
+ - Add support to cherry-pick any commit into any branch in the web interface (Minqi Pan)
- Project switcher uses new dropdown styling
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
- Restrict user profiles when public visibility level is restricted.
+ - Add ability set due date to issues, sort and filter issues by due date (Mehmet Beydogan)
- All images in discussions and wikis now link to their source files !3464 (Connor Shea).
- Return status code 303 after a branch DELETE operation to avoid project deletion (Stan Hu)
- Add setting for customizing the list of trusted proxies !3524
- Allow projects to be transfered to a lower visibility level group
- Fix `signed_in_ip` being set to 127.0.0.1 when using a reverse proxy !3524
- Improved Markdown rendering performance !3389
+ - Make shared runners text in box configurable
- Don't attempt to look up an avatar in repo if repo directory does not exist (Stan Hu)
- API: Ability to subscribe and unsubscribe from issues and merge requests (Robert Schilling)
- Expose project badges in project settings
- Make /profile/keys/new redirect to /profile/keys for back-compat. !3717
- Preserve time notes/comments have been updated at when moving issue
- Make HTTP(s) label consistent on clone bar (Stan Hu)
+ - Add support for `after_script`, requires Runner 1.2 (Kamil Trzciński)
- Expose label description in API (Mariusz Jachimowicz)
- API: Ability to update a group (Robert Schilling)
- API: Ability to move issues (Robert Schilling)
@@ -34,6 +41,8 @@ v 8.7.0 (unreleased)
- Fix a bug whith trailing slash in teamcity_url (Charles May)
- Allow back dating on issues when created or updated through the API
- Allow back dating on issue notes when created through the API
+ - Propose license template when creating a new LICENSE file
+ - API: Expose /licenses and /licenses/:key
- Fix avatar stretching by providing a cropping feature
- API: Expose `subscribed` for issues and merge requests (Robert Schilling)
- Allow SAML to handle external users based on user's information !3530
@@ -51,6 +60,7 @@ v 8.7.0 (unreleased)
- Use rugged to change HEAD in Project#change_head (P.S.V.R)
- API: Ability to filter milestones by state `active` and `closed` (Robert Schilling)
- API: Fix milestone filtering by `iid` (Robert Schilling)
+ - Make before_script and after_script overridable on per-job (Kamil Trzciński)
- API: Delete notes of issues, snippets, and merge requests (Robert Schilling)
- Implement 'Groups View' as an option for dashboard preferences !3379 (Elias W.)
- Better errors handling when creating milestones inside groups
@@ -71,29 +81,48 @@ v 8.7.0 (unreleased)
- API: Do not leak group existence via return code (Robert Schilling)
- ClosingIssueExtractor regex now also works with colons. e.g. "Fixes: #1234" !3591
- Update number of Todos in the sidebar when it's marked as "Done". !3600
+ - Sanitize branch names created for confidential issues
- API: Expose 'updated_at' for issue, snippet, and merge request notes (Robert Schilling)
- API: User can leave a project through the API when not master or owner. !3613
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms
+ - Show JavaScript errors in sentry
+ - Diff design updates (colors, button styles, etc)
+ - Copying and pasting a diff no longer pastes the line numbers or +/-
+ - Add null check to formData when updating profile content to fix Firefox bug
+ - Disable spellcheck and autocorrect for username field in admin page
- Delete tags using Rugged for performance reasons (Robert Schilling)
+ - Add Slack notifications when Wiki is edited (Sebastian Klier)
- Diffs load at the correct point when linking from from number
- Selected diff rows highlight
- Fix emoji categories in the emoji picker
+ - API: Properly display annotated tags for GET /projects/:id/repository/tags (Robert Schilling)
- Add encrypted credentials for imported projects and migrate old ones
+ - Properly format all merge request references with ! rather than # !3740 (Ben Bodenmiller)
- Author and participants are displayed first on users autocompletion
- Show number sign on external issue reference text (Florent Baldino)
- Updated print style for issues
- Use GitHub Issue/PR number as iid to keep references
- Import GitHub labels
- Import GitHub milestones
+ - Fix emoji catgories in the emoji picker
+ - Execute system web hooks on push to the project
+ - Allow enable/disable push events for system hooks
+ - Fix GitHub project's link in the import page when provider has a custom URL
+ - Add RAW build trace output and button on build page
+ - Add incremental build trace update into CI API
+
+v 8.6.7
+ - Fix persistent XSS vulnerability in `commit_person_link` helper
+ - Fix persistent XSS vulnerability in Label and Milestone dropdowns
+ - Fix vulnerability that made it possible to enumerate private projects belonging to group
v 8.6.6
- Expire the exists cache before deletion to ensure project dir actually exists (Stan Hu). !3413
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
- Fix revoking of authorized OAuth applications (Connor Shea). !3690
- Fix error on language detection when repository has no HEAD (e.g., master branch). !3654 (Jeroen Bobbeldijk)
- - Project switcher uses new dropdown styling
- Issuable header is consistent between issues and merge requests
- Improved spacing in issuable header on mobile
@@ -225,6 +254,9 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
+v 8.5.11
+ - Fix persistent XSS vulnerability in `commit_person_link` helper
+
v 8.5.10
- Fix a 2FA authentication spoofing vulnerability.
@@ -372,6 +404,9 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
+v 8.4.9
+ - Fix persistent XSS vulnerability in `commit_person_link` helper
+
v 8.4.8
- Fix a 2FA authentication spoofing vulnerability.
@@ -494,6 +529,9 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
+v 8.3.8
+ - Fix persistent XSS vulnerability in `commit_person_link` helper
+
v 8.3.7
- Fix a 2FA authentication spoofing vulnerability.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 1f26a5d7eaf..24cd5864530 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -323,6 +323,7 @@ request is as follows:
[shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md).
+1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
The **official merge window** is in the beginning of the month from the 1st to
the 7th day of the month. This is the best time to submit an MR and get
diff --git a/Gemfile b/Gemfile
index 8caa749a8d4..67cc3f34b8c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -190,6 +190,9 @@ gem 'babosa', '~> 1.0.2'
# Sanitizes SVG input
gem "loofah", "~> 2.0.3"
+# Working with license
+gem 'licensee', '~> 8.0.0'
+
# Protect against bruteforcing
gem "rack-attack", '~> 4.3.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 958824f7ed9..b00d7b35c84 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -452,6 +452,8 @@ GEM
addressable (~> 2.3)
letter_opener (1.1.2)
launchy (~> 2.2)
+ licensee (8.0.0)
+ rugged (>= 0.24b)
listen (3.0.5)
rb-fsevent (>= 0.9.3)
rb-inotify (>= 0.9)
@@ -957,6 +959,7 @@ DEPENDENCIES
jquery-ui-rails (~> 5.0.0)
kaminari (~> 0.16.3)
letter_opener (~> 1.1.2)
+ licensee (~> 8.0.0)
loofah (~> 2.0.3)
mail_room (~> 0.6.1)
method_source (~> 0.8)
diff --git a/VERSION b/VERSION
index 91ab1f99daf..d5a967c3933 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-8.7.0-pre
+8.8.0-pre
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index f3ed9a66715..dd1bbb37551 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -5,6 +5,7 @@
group_projects_path: "/api/:version/groups/:id/projects.json"
projects_path: "/api/:version/projects.json"
labels_path: "/api/:version/projects/:id/labels"
+ license_path: "/api/:version/licenses/:key"
group: (group_id, callback) ->
url = Api.buildUrl(Api.group_path)
@@ -92,6 +93,16 @@
).done (projects) ->
callback(projects)
+ # Return text for a specific license
+ licenseText: (key, data, callback) ->
+ url = Api.buildUrl(Api.license_path).replace(':key', key)
+
+ $.ajax(
+ url: url
+ data: data
+ ).done (license) ->
+ callback(license)
+
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 5bac8eef1cb..642e7429acf 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -55,6 +55,7 @@
#= require_tree .
#= require fuzzaldrin-plus
#= require cropper
+#= require raven
window.slugify = (text) ->
text.replace(/[^-a-zA-Z0-9]+/g, '_').toLowerCase()
diff --git a/app/assets/javascripts/blob/blob_license_selector.js.coffee b/app/assets/javascripts/blob/blob_license_selector.js.coffee
new file mode 100644
index 00000000000..e17eaa75dc1
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selector.js.coffee
@@ -0,0 +1,30 @@
+class @BlobLicenseSelector
+ licenseRegex: /^(.+\/)?(licen[sc]e|copying)($|\.)/i
+
+ constructor: (editor) ->
+ @$licenseSelector = $('.js-license-selector')
+ $fileNameInput = $('#file_name')
+
+ initialFileNameValue = if $fileNameInput.length
+ $fileNameInput.val()
+ else if $('.editor-file-name').length
+ $('.editor-file-name').text().trim()
+
+ @toggleLicenseSelector(initialFileNameValue)
+
+ if $fileNameInput
+ $fileNameInput.on 'keyup blur', (e) =>
+ @toggleLicenseSelector($(e.target).val())
+
+ $('select.license-select').on 'change', (e) ->
+ data =
+ project: $(this).data('project')
+ fullname: $(this).data('fullname')
+ Api.licenseText $(this).val(), data, (license) ->
+ editor.setValue(license.content, -1)
+
+ toggleLicenseSelector: (fileName) =>
+ if @licenseRegex.test(fileName)
+ @$licenseSelector.show()
+ else
+ @$licenseSelector.hide()
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index 390e41ed8d4..eea9aa972ee 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -1,44 +1,39 @@
class @EditBlob
- constructor: (assets_path, mode)->
- ace.config.set "modePath", assets_path + '/ace'
+ constructor: (assets_path, ace_mode = null) ->
+ ace.config.set "modePath", "#{assets_path}/ace"
ace.config.loadModule "ace/ext/searchbox"
- if mode
- ace_mode = mode
- editor = ace.edit("editor")
- editor.focus()
- @editor = editor
-
- if ace_mode
- editor.getSession().setMode "ace/mode/" + ace_mode
+ @editor = ace.edit("editor")
+ @editor.focus()
+ @editor.getSession().setMode "ace/mode/#{ace_mode}" if ace_mode
# Before a form submission, move the content from the Ace editor into the
# submitted textarea
- $('form').submit ->
- $("#file-content").val(editor.getValue())
+ $('form').submit =>
+ $("#file-content").val(@editor.getValue())
+
+ @initModePanesAndLinks()
+ new BlobLicenseSelector(@editor)
- editModePanes = $(".js-edit-mode-pane")
- editModeLinks = $(".js-edit-mode a")
- editModeLinks.click (event) ->
- event.preventDefault()
- currentLink = $(this)
- paneId = currentLink.attr("href")
- currentPane = editModePanes.filter(paneId)
- editModeLinks.parent().removeClass "active hover"
- currentLink.parent().addClass "active hover"
- editModePanes.hide()
- if paneId is "#preview"
- currentPane.fadeIn 200
- $.post currentLink.data("preview-url"),
- content: editor.getValue()
- , (response) ->
- currentPane.empty().append response
- currentPane.syntaxHighlight()
- return
+ initModePanesAndLinks: ->
+ @$editModePanes = $(".js-edit-mode-pane")
+ @$editModeLinks = $(".js-edit-mode a")
+ @$editModeLinks.click @editModeLinkClickHandler
- else
- currentPane.fadeIn 200
- editor.focus()
- return
+ editModeLinkClickHandler: (event) =>
+ event.preventDefault()
+ currentLink = $(event.target)
+ paneId = currentLink.attr("href")
+ currentPane = @$editModePanes.filter(paneId)
+ @$editModeLinks.parent().removeClass "active hover"
+ currentLink.parent().addClass "active hover"
+ @$editModePanes.hide()
+ currentPane.fadeIn 200
+ if paneId is "#preview"
+ $.post currentLink.data("preview-url"),
+ content: @editor.getValue()
+ , (response) ->
+ currentPane.empty().append response
+ currentPane.syntaxHighlight()
- editor: ->
- return @editor
+ else
+ @editor.focus()
diff --git a/app/assets/javascripts/blob/new_blob.js.coffee b/app/assets/javascripts/blob/new_blob.js.coffee
deleted file mode 100644
index 68c5e5195e3..00000000000
--- a/app/assets/javascripts/blob/new_blob.js.coffee
+++ /dev/null
@@ -1,20 +0,0 @@
-class @NewBlob
- constructor: (assets_path, mode)->
- ace.config.set "modePath", assets_path + '/ace'
- ace.config.loadModule "ace/ext/searchbox"
- if mode
- ace_mode = mode
- editor = ace.edit("editor")
- editor.focus()
- @editor = editor
-
- if ace_mode
- editor.getSession().setMode "ace/mode/" + ace_mode
-
- # Before a form submission, move the content from the Ace editor into the
- # submitted textarea
- $('form').submit ->
- $("#file-content").val(editor.getValue())
-
- editor: ->
- return @editor
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index 0b9110d35fa..2fdb7562515 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -17,6 +17,7 @@ class Dispatcher
switch page
when 'projects:issues:index'
Issues.init()
+ Issuable.init()
shortcut_handler = new ShortcutsNavigation()
when 'projects:issues:show'
new Issue()
@@ -57,7 +58,7 @@ class Dispatcher
new ZenMode()
when 'projects:merge_requests:index'
shortcut_handler = new ShortcutsNavigation()
- MergeRequests.init()
+ Issuable.init()
when 'dashboard:activity'
new Activities()
when 'dashboard:projects:starred'
diff --git a/app/assets/javascripts/due_date_select.js.coffee b/app/assets/javascripts/due_date_select.js.coffee
new file mode 100644
index 00000000000..a4304786cbb
--- /dev/null
+++ b/app/assets/javascripts/due_date_select.js.coffee
@@ -0,0 +1,64 @@
+class @DueDateSelect
+ constructor: ->
+ $loading = $('.js-issuable-update .due_date')
+ .find('.block-loading')
+ .hide()
+
+ $('.js-due-date-select').each (i, dropdown) ->
+ $dropdown = $(dropdown)
+ $dropdownParent = $dropdown.closest('.dropdown')
+ $datePicker = $dropdownParent.find('.js-due-date-calendar')
+ $block = $dropdown.closest('.block')
+ $selectbox = $dropdown.closest('.selectbox')
+ $value = $block.find('.value')
+ $sidebarValue = $('.js-due-date-sidebar-value', $block)
+
+ fieldName = $dropdown.data('field-name')
+ abilityName = $dropdown.data('ability-name')
+ issueUpdateURL = $dropdown.data('issue-update')
+
+ $dropdown.glDropdown(
+ hidden: ->
+ $selectbox.hide()
+ $value.removeAttr('style')
+ )
+
+ addDueDate = ->
+ # Create the post date
+ value = $("input[name='#{fieldName}']").val()
+ date = new Date value.replace(new RegExp('-', 'g'), ',')
+ mediumDate = $.datepicker.formatDate 'M d, yy', date
+
+ data = {}
+ data[abilityName] = {}
+ data[abilityName].due_date = value
+
+ $.ajax(
+ type: 'PUT'
+ url: issueUpdateURL
+ data: data
+ beforeSend: ->
+ $loading.fadeIn()
+ $dropdown.trigger('loading.gl.dropdown')
+ $selectbox.hide()
+ $value.removeAttr('style')
+
+ $value.html(mediumDate)
+ $sidebarValue.html(mediumDate)
+ ).done (data) ->
+ $dropdown.trigger('loaded.gl.dropdown')
+ $dropdown.dropdown('toggle')
+ $loading.fadeOut()
+
+ $datePicker.datepicker(
+ dateFormat: 'yy-mm-dd',
+ defaultDate: $("input[name='#{fieldName}']").val()
+ altField: "input[name='#{fieldName}']"
+ onSelect: ->
+ addDueDate()
+ )
+
+ $(document)
+ .off 'click', '.ui-datepicker-header a'
+ .on 'click', '.ui-datepicker-header a', (e) ->
+ e.stopImmediatePropagation()
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index fac928ef202..e5204f9dee9 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -154,6 +154,9 @@ class GitLabDropdown
@fullData = data
@parseData @fullData
+
+ if @options.filterable
+ @filterInput.trigger 'keyup'
}
# Init filterable
@@ -383,13 +386,13 @@ class GitLabDropdown
else
selectedObject
else
- if !value?
- field.remove()
-
- if not @options.multiSelect
+ if not @options.multiSelect or el.hasClass('dropdown-clear-active')
@dropdown.find(".#{ACTIVE_CLASS}").removeClass ACTIVE_CLASS
@dropdown.parent().find("input[name='#{fieldName}']").remove()
+ if !value?
+ field.remove()
+
# Toggle active class for the tick mark
el.addClass ACTIVE_CLASS
diff --git a/app/assets/javascripts/issuable.js.coffee b/app/assets/javascripts/issuable.js.coffee
new file mode 100644
index 00000000000..afffed63ac5
--- /dev/null
+++ b/app/assets/javascripts/issuable.js.coffee
@@ -0,0 +1,84 @@
+@Issuable =
+ init: ->
+ Issuable.initTemplates()
+ Issuable.initSearch()
+
+ initTemplates: ->
+ Issuable.labelRow = _.template(
+ '<% _.each(labels, function(label){ %>
+ <span class="label-row">
+ <a href="#"><span class="label color-label has-tooltip" style="background-color: <%= label.color %>; color: <%= label.text_color %>" title="<%= _.escape(label.description) %>" data-container="body"><%= _.escape(label.title) %></span></a>
+ </span>
+ <% }); %>'
+ )
+
+ initSearch: ->
+ @timer = null
+ $('#issue_search')
+ .off 'keyup'
+ .on 'keyup', ->
+ clearTimeout(@timer)
+ @timer = setTimeout( ->
+ Issuable.filterResults $('#issue_search_form')
+ , 500)
+
+ toggleLabelFilters: ->
+ $filteredLabels = $('.filtered-labels')
+ if $filteredLabels.find('.label-row').length > 0
+ $filteredLabels.removeClass('hidden')
+ else
+ $filteredLabels.addClass('hidden')
+
+ filterResults: (form) =>
+ formData = form.serialize()
+
+ $('.issues-holder, .merge-requests-holder').css('opacity', '0.5')
+ formAction = form.attr('action')
+ issuesUrl = formAction
+ issuesUrl += ("#{if formAction.indexOf('?') < 0 then '?' else '&'}")
+ issuesUrl += formData
+ $.ajax
+ type: 'GET'
+ url: formAction
+ data: formData
+ complete: ->
+ $('.issues-holder, .merge-requests-holder').css('opacity', '1.0')
+ success: (data) ->
+ $('.issues-holder, .merge-requests-holder').html(data.html)
+ # Change url so if user reload a page - search results are saved
+ history.replaceState {page: issuesUrl}, document.title, issuesUrl
+ Issuable.reload()
+ Issuable.updateStateFilters()
+ $filteredLabels = $('.filtered-labels')
+
+ if typeof Issuable.labelRow is 'function'
+ $filteredLabels.html(Issuable.labelRow(data))
+
+ Issuable.toggleLabelFilters()
+
+ dataType: "json"
+
+ reload: ->
+ if Issues.created
+ Issues.initChecks()
+
+ $('#filter_issue_search').val($('#issue_search').val())
+
+ updateStateFilters: ->
+ stateFilters = $('.issues-state-filters')
+ newParams = {}
+ paramKeys = ['author_id', 'milestone_title', 'assignee_id', 'issue_search']
+
+ for paramKey in paramKeys
+ newParams[paramKey] = gl.utils.getParameterValues(paramKey)[0] or ''
+
+ if stateFilters.length
+ stateFilters.find('a').each ->
+ initialUrl = gl.utils.removeParamQueryString($(this).attr('href'), 'label_name[]')
+ labelNameValues = gl.utils.getParameterValues('label_name[]')
+ if labelNameValues
+ labelNameQueryString = ("label_name[]=#{value}" for value in labelNameValues).join('&')
+ newUrl = "#{gl.utils.mergeUrlParams(newParams, initialUrl)}&#{labelNameQueryString}"
+ else
+ newUrl = gl.utils.mergeUrlParams(newParams, initialUrl)
+ $(this).attr 'href', newUrl
diff --git a/app/assets/javascripts/issuable_context.js.coffee b/app/assets/javascripts/issuable_context.js.coffee
index 2f19513a831..3a439b94c59 100644
--- a/app/assets/javascripts/issuable_context.js.coffee
+++ b/app/assets/javascripts/issuable_context.js.coffee
@@ -9,21 +9,29 @@ class @IssuableContext
$(".issuable-sidebar .inline-update").on "change", ".js-assignee", ->
$(this).submit()
- $(document).off("click", ".edit-link").on "click",".edit-link", (e) ->
- $block = $(@).parents('.block')
- $selectbox = $block.find('.selectbox')
- if $selectbox.is(':visible')
- $selectbox.hide()
- $block.find('.value').show()
- else
- $selectbox.show()
- $block.find('.value').hide()
-
- if $selectbox.is(':visible')
- setTimeout (->
- $block.find('.dropdown-menu-toggle').trigger 'click'
- ), 0
-
+ $(document)
+ .off 'click', '.dropdown-content a'
+ .on 'click', '.dropdown-content a', (e) ->
+ e.preventDefault()
+
+ $(document)
+ .off 'click', '.edit-link'
+ .on 'click', '.edit-link', (e) ->
+ e.preventDefault()
+
+ $block = $(@).parents('.block')
+ $selectbox = $block.find('.selectbox')
+ if $selectbox.is(':visible')
+ $selectbox.hide()
+ $block.find('.value').show()
+ else
+ $selectbox.show()
+ $block.find('.value').hide()
+
+ if $selectbox.is(':visible')
+ setTimeout ->
+ $block.find('.dropdown-menu-toggle').trigger 'click'
+ , 0
$(".right-sidebar").niceScroll()
diff --git a/app/assets/javascripts/issues.js.coffee b/app/assets/javascripts/issues.js.coffee
index 0d9f2094c2a..3330e6c68ad 100644
--- a/app/assets/javascripts/issues.js.coffee
+++ b/app/assets/javascripts/issues.js.coffee
@@ -1,6 +1,6 @@
@Issues =
init: ->
- Issues.initSearch()
+ Issues.created = true
Issues.initChecks()
$("body").on "ajax:success", ".close_issue, .reopen_issue", ->
@@ -15,10 +15,6 @@
else
$(this).html totalIssues - 1
- reload: ->
- Issues.initChecks()
- $('#filter_issue_search').val($('#issue_search').val())
-
initChecks: ->
$(".check_all_issues").click ->
$(".selected_issue").prop("checked", @checked)
@@ -26,51 +22,6 @@
$(".selected_issue").bind "change", Issues.checkChanged
- # Update state filters if present in page
- updateStateFilters: ->
- stateFilters = $('.issues-state-filters')
- newParams = {}
- paramKeys = ['author_id', 'label_name', 'milestone_title', 'assignee_id', 'issue_search']
-
- for paramKey in paramKeys
- newParams[paramKey] = gl.utils.getUrlParameter(paramKey) or ''
-
- if stateFilters.length
- stateFilters.find('a').each ->
- initialUrl = $(this).attr 'href'
- $(this).attr 'href', gl.utils.mergeUrlParams(newParams, initialUrl)
-
- # Make sure we trigger ajax request only after user stop typing
- initSearch: ->
- @timer = null
- $("#issue_search").keyup ->
- clearTimeout(@timer)
- @timer = setTimeout( ->
- Issues.filterResults $("#issue_search_form")
- , 500)
-
- filterResults: (form) =>
- $('.issues-holder, .merge-requests-holder').css("opacity", '0.5')
- formAction = form.attr('action')
- formData = form.serialize()
- issuesUrl = formAction
- issuesUrl += ("#{if formAction.indexOf("?") < 0 then '?' else '&'}")
- issuesUrl += formData
-
- $.ajax
- type: "GET"
- url: formAction
- data: formData
- complete: ->
- $('.issues-holder, .merge-requests-holder').css("opacity", '1.0')
- success: (data) ->
- $('.issues-holder, .merge-requests-holder').html(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: issuesUrl}, document.title, issuesUrl
- Issues.reload()
- Issues.updateStateFilters()
- dataType: "json"
-
checkChanged: ->
checked_issues = $(".selected_issue:checked")
if checked_issues.length > 0
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index bc80980acb7..021ade73d44 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -6,7 +6,7 @@ class @LabelsSelect
labelUrl = $dropdown.data('labels')
issueUpdateURL = $dropdown.data('issueUpdate')
selectedLabel = $dropdown.data('selected')
- if selectedLabel?
+ if selectedLabel? and not $dropdown.hasClass 'js-multiselect'
selectedLabel = selectedLabel.split(',')
newLabelField = $('#new_label_name')
newColorField = $('#new_label_color')
@@ -16,6 +16,7 @@ class @LabelsSelect
abilityName = $dropdown.data('ability-name')
$selectbox = $dropdown.closest('.selectbox')
$block = $selectbox.closest('.block')
+ $form = $dropdown.closest('form')
$sidebarCollapsedValue = $block.find('.sidebar-collapsed-icon span')
$value = $block.find('.value')
$loading = $block.find('.block-loading').fadeOut()
@@ -33,13 +34,13 @@ class @LabelsSelect
if issueUpdateURL
labelHTMLTemplate = _.template(
'<% _.each(labels, function(label){ %>
- <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= label.title %>">
- <span class="label has-tooltip color-label" title="<%= label.description %>" style="background-color: <%= label.color %>;">
- <%= label.title %>
+ <a href="<%= ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name=<%= _.escape(label.title) %>">
+ <span class="label has-tooltip color-label" title="<%= _.escape(label.description) %>" style="background-color: <%= label.color %>;">
+ <%= _.escape(label.title) %>
</span>
</a>
<% }); %>'
- );
+ )
labelNoneHTMLTemplate = _.template('<div class="light">None</div>')
if newLabelField.length and $dropdown.hasClass 'js-extra-options'
@@ -171,7 +172,7 @@ class @LabelsSelect
.find('a')
.each((i) ->
setTimeout(=>
- glAnimate($(@), 'pulse')
+ gl.animate.animate($(@), 'pulse')
,200 * i
)
)
@@ -200,18 +201,23 @@ class @LabelsSelect
callback data
renderRow: (label) ->
- selectedClass = ''
- if $selectbox.find("input[type='hidden']\
- [name='#{$dropdown.data('field-name')}']\
- [value='#{label.id}']").length
- selectedClass = 'is-active'
+ removesAll = label.id is 0 or not label.id?
+
+ selectedClass = []
+ if $form.find("input[type='hidden']\
+ [name='#{$dropdown.data('fieldName')}']\
+ [value='#{this.id(label)}']").length
+ selectedClass.push 'is-active'
+
+ if $dropdown.hasClass('js-multiselect') and removesAll
+ selectedClass.push 'dropdown-clear-active'
color = if label.color? then "<span class='dropdown-label-box' style='background-color: #{label.color}'></span>" else ""
"<li>
- <a href='#' class='#{selectedClass}'>
+ <a href='#' class='#{selectedClass.join(' ')}'>
#{color}
- #{label.title}
+ #{_.escape(label.title)}
</a>
</li>"
filterable: true
@@ -219,37 +225,56 @@ class @LabelsSelect
fields: ['title']
selectable: true
- toggleLabel: (selected) ->
+ toggleLabel: (selected, el) ->
+ selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active')
+
if selected and selected.title?
- selected.title
+ if selected_labels.length > 1
+ "#{selected.title} +#{selected_labels.length - 1} more"
+ else
+ selected.title
+ else if not selected and selected_labels.length isnt 0
+ if selected_labels.length > 1
+ "#{$(selected_labels[0]).text()} +#{selected_labels.length - 1} more"
+ else if selected_labels.length is 1
+ $(selected_labels).text()
else
defaultLabel
fieldName: $dropdown.data('field-name')
id: (label) ->
- if label.isAny?
- ''
- else if $dropdown.hasClass "js-filter-submit"
+ if $dropdown.hasClass("js-filter-submit") and not label.isAny?
label.title
else
label.id
hidden: ->
+ page = $('body').data 'page'
+ isIssueIndex = page is 'projects:issues:index'
+ isMRIndex = page is 'projects:merge_requests:index'
+
$selectbox.hide()
# display:block overrides the hide-collapse rule
$value.removeAttr('style')
if $dropdown.hasClass 'js-multiselect'
- saveLabelData()
+ if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
+ selectedLabels = $dropdown
+ .closest('form')
+ .find("input:hidden[name='#{$dropdown.data('fieldName')}']")
+ Issuable.filterResults $dropdown.closest('form')
+ else if $dropdown.hasClass('js-filter-submit')
+ $dropdown.closest('form').submit()
+ else
+ saveLabelData()
multiSelect: $dropdown.hasClass 'js-multiselect'
clicked: (label) ->
page = $('body').data 'page'
isIssueIndex = page is 'projects:issues:index'
- isMRIndex = page is page is 'projects:merge_requests:index'
-
+ isMRIndex = page is 'projects:merge_requests:index'
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
- selectedLabel = label.title
-
- Issues.filterResults $dropdown.closest('form')
+ if not $dropdown.hasClass 'js-multiselect'
+ selectedLabel = label.title
+ Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit()
else
diff --git a/app/assets/javascripts/lib/animate.js.coffee b/app/assets/javascripts/lib/animate.js.coffee
index 8f892b5a2b9..ec3b44d6126 100644
--- a/app/assets/javascripts/lib/animate.js.coffee
+++ b/app/assets/javascripts/lib/animate.js.coffee
@@ -1,13 +1,39 @@
((w) ->
+ if not w.gl? then w.gl = {}
+ if not gl.animate? then gl.animate = {}
- w.glAnimate = ($el, animation, done) ->
+ gl.animate.animate = ($el, animation, options, done) ->
+ if options?.cssStart?
+ $el.css(options.cssStart)
$el
- .removeClass()
+ .removeClass(animation + ' animated')
.addClass(animation + ' animated')
.one 'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend', ->
- $(this).removeClass()
+ $(this).removeClass(animation + ' animated')
+ if done?
+ done()
+ if options?.cssEnd?
+ $el.css(options.cssEnd)
return
return
- return
+ gl.animate.animateEach = ($els, animation, time, options, done) ->
+ dfd = $.Deferred()
+ if not $els.length
+ dfd.resolve()
+ $els.each((i) ->
+ setTimeout(=>
+ $this = $(@)
+ gl.animate.animate($this, animation, options, =>
+ if i is $els.length - 1
+ dfd.resolve()
+ if done?
+ done()
+ )
+ ,time * i
+ )
+ return
+ )
+ return dfd.promise()
+ return
) window \ No newline at end of file
diff --git a/app/assets/javascripts/lib/url_utility.js.coffee b/app/assets/javascripts/lib/url_utility.js.coffee
index abd556e0b4e..6a00932c028 100644
--- a/app/assets/javascripts/lib/url_utility.js.coffee
+++ b/app/assets/javascripts/lib/url_utility.js.coffee
@@ -3,16 +3,20 @@
w.gl ?= {}
w.gl.utils ?= {}
- w.gl.utils.getUrlParameter = (sParam) ->
+ # Returns an array containing the value(s) of the
+ # of the key passed as an argument
+ w.gl.utils.getParameterValues = (sParam) ->
sPageURL = decodeURIComponent(window.location.search.substring(1))
sURLVariables = sPageURL.split('&')
sParameterName = undefined
+ values = []
i = 0
while i < sURLVariables.length
sParameterName = sURLVariables[i].split('=')
if sParameterName[0] is sParam
- return if sParameterName[1] is undefined then true else sParameterName[1]
+ values.push(sParameterName[1])
i++
+ values
# #
# @param {Object} params - url keys and value to merge
@@ -28,4 +32,12 @@
newUrl = "#{newUrl}#{(if newUrl.indexOf('?') > 0 then '&' else '?')}#{paramName}=#{paramValue}"
newUrl
+ # removes parameter query string from url. returns the modified url
+ w.gl.utils.removeParamQueryString = (url, param) ->
+ url = decodeURIComponent(url)
+ urlVariables = url.split('&')
+ (
+ variables for variables in urlVariables when variables.indexOf(param) is -1
+ ).join('&')
+
) window
diff --git a/app/assets/javascripts/merge_requests.js.coffee b/app/assets/javascripts/merge_requests.js.coffee
deleted file mode 100644
index b3c73ffce5d..00000000000
--- a/app/assets/javascripts/merge_requests.js.coffee
+++ /dev/null
@@ -1,35 +0,0 @@
-#
-# * Filter merge requests
-#
-@MergeRequests =
- init: ->
- MergeRequests.initSearch()
-
- # Make sure we trigger ajax request only after user stop typing
- initSearch: ->
- @timer = null
- $("#issue_search").keyup ->
- clearTimeout(@timer)
- @timer = setTimeout(MergeRequests.filterResults, 500)
-
- filterResults: =>
- form = $("#issue_search_form")
- search = $("#issue_search").val()
- $('.merge-requests-holder').css("opacity", '0.5')
- issues_url = form.attr('action') + '?' + form.serialize()
-
- $.ajax
- type: "GET"
- url: form.attr('action')
- data: form.serialize()
- complete: ->
- $('.merge-requests-holder').css("opacity", '1.0')
- success: (data) ->
- $('.merge-requests-holder').html(data.html)
- # Change url so if user reload a page - search results are saved
- history.replaceState {page: issues_url}, document.title, issues_url
- MergeRequests.reload()
- dataType: "json"
-
- reload: ->
- $('#filter_issue_search').val($('#issue_search').val())
diff --git a/app/assets/javascripts/milestone_select.js.coffee b/app/assets/javascripts/milestone_select.js.coffee
index 6bd4e885a03..345a0e447af 100644
--- a/app/assets/javascripts/milestone_select.js.coffee
+++ b/app/assets/javascripts/milestone_select.js.coffee
@@ -24,7 +24,7 @@ class @MilestoneSelect
if issueUpdateURL
milestoneLinkTemplate = _.template(
- '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= title %></a>'
+ '<a href="/<%= namespace %>/<%= path %>/milestones/<%= iid %>"><%= _.escape(title) %></a>'
)
milestoneLinkNoneTemplate = '<div class="light">None</div>'
@@ -71,7 +71,7 @@ class @MilestoneSelect
defaultLabel
fieldName: $dropdown.data('field-name')
text: (milestone) ->
- milestone.title
+ _.escape(milestone.title)
id: (milestone) ->
if !useId
milestone.name
@@ -97,7 +97,7 @@ class @MilestoneSelect
selectedMilestone = selected.name
else
selectedMilestone = ''
- Issues.filterResults $dropdown.closest('form')
+ Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass('js-filter-submit')
$dropdown.closest('form').submit()
else
diff --git a/app/assets/javascripts/raven_config.js.coffee b/app/assets/javascripts/raven_config.js.coffee
new file mode 100644
index 00000000000..d031a655abf
--- /dev/null
+++ b/app/assets/javascripts/raven_config.js.coffee
@@ -0,0 +1,44 @@
+@raven =
+ init: ->
+ if gon.sentry_dsn?
+ Raven.config(gon.sentry_dsn, {
+ includePaths: [/gon.relative_url_root/]
+ ignoreErrors: [
+ # Random plugins/extensions
+ 'top.GLOBALS',
+ # See: http://blog.errorception.com/2012/03/tale-of-unfindable-js-error. html
+ 'originalCreateNotification',
+ 'canvas.contentDocument',
+ 'MyApp_RemoveAllHighlights',
+ 'http://tt.epicplay.com',
+ 'Can\'t find variable: ZiteReader',
+ 'jigsaw is not defined',
+ 'ComboSearch is not defined',
+ 'http://loading.retry.widdit.com/',
+ 'atomicFindClose',
+ # ISP "optimizing" proxy - `Cache-Control: no-transform` seems to
+ # reduce this. (thanks @acdha)
+ # See http://stackoverflow.com/questions/4113268
+ 'bmi_SafeAddOnload',
+ 'EBCallBackMessageReceived',
+ # See http://toolbar.conduit.com/Developer/HtmlAndGadget/Methods/JSInjection.aspx
+ 'conduitPage'
+ ],
+ ignoreUrls: [
+ # Chrome extensions
+ /extensions\//i,
+ /^chrome:\/\//i,
+ # Other plugins
+ /127\.0\.0\.1:4001\/isrunning/i, # Cacaoweb
+ /webappstoolbarba\.texthelp\.com\//i,
+ /metrics\.itunes\.apple\.com\.edgesuite\.net\//i
+ ]
+ }).install()
+
+ if gon.current_user_id
+ Raven.setUserContext({
+ id: gon.current_user_id
+ })
+
+$ ->
+ raven.init()
diff --git a/app/assets/javascripts/todos.js.coffee b/app/assets/javascripts/todos.js.coffee
index 00d2b641723..10e698d6a54 100644
--- a/app/assets/javascripts/todos.js.coffee
+++ b/app/assets/javascripts/todos.js.coffee
@@ -1,5 +1,11 @@
class @Todos
- constructor: (@name) ->
+ constructor: (opts = {}) ->
+ {
+ @el = $('.js-todos-options')
+ } = opts
+
+ @perPage = @el.data('perPage')
+
@clearListeners()
@initBtnListeners()
@@ -26,6 +32,7 @@ class @Todos
dataType: 'json'
data: '_method': 'delete'
success: (data) =>
+ @redirectIfNeeded data.count
@clearDone $this.closest('li')
@updateBadges data
@@ -57,6 +64,40 @@ class @Todos
$('.todos-pending .badge, .todos-pending-count').text data.count
$('.todos-done .badge').text data.done_count
+ getTotalPages: ->
+ @el.data('totalPages')
+
+ getCurrentPage: ->
+ @el.data('currentPage')
+
+ getTodosPerPage: ->
+ @el.data('perPage')
+
+ redirectIfNeeded: (total) ->
+ currPages = @getTotalPages()
+ currPage = @getCurrentPage()
+
+ # Refresh if no remaining Todos
+ if not total
+ location.reload()
+ return
+
+ # Do nothing if no pagination
+ return if not currPages
+
+ newPages = Math.ceil(total / @getTodosPerPage())
+ url = location.href # Includes query strings
+
+ # If new total of pages is different than we have now
+ if newPages isnt currPages
+ # Redirect to previous page if there's one available
+ if currPages > 1 and currPage is currPages
+ pageParams =
+ page: currPages - 1
+ url = gl.utils.mergeUrlParams(pageParams, url)
+
+ Turbolinks.visit(url)
+
goToTodoUrl: (e)->
todoLink = $(this).data('url')
return unless todoLink
diff --git a/app/assets/javascripts/users_select.js.coffee b/app/assets/javascripts/users_select.js.coffee
index a7e934936e9..b80b1b861cc 100644
--- a/app/assets/javascripts/users_select.js.coffee
+++ b/app/assets/javascripts/users_select.js.coffee
@@ -158,7 +158,7 @@ class @UsersSelect
if $dropdown.hasClass('js-filter-submit') and (isIssueIndex or isMRIndex)
selectedId = user.id
- Issues.filterResults $dropdown.closest('form')
+ Issuable.filterResults $dropdown.closest('form')
else if $dropdown.hasClass 'js-filter-submit'
$dropdown.closest('form').submit()
else
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index ba6c7930cdc..fd395041c3d 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -248,7 +248,7 @@
.dropdown-title {
position: relative;
- padding: 0 25px 15px;
+ padding: 0 25px 10px;
margin: 0 10px 10px;
font-weight: 600;
line-height: 1;
@@ -278,7 +278,7 @@
right: 5px;
width: 20px;
height: 20px;
- top: -1px;
+ top: -3px;
}
.dropdown-menu-back {
@@ -358,6 +358,13 @@
border-top: 1px solid $dropdown-divider-color;
}
+.dropdown-due-date-footer {
+ padding-top: 0;
+ margin-left: 10px;
+ margin-right: 10px;
+ border-top: 0;
+}
+
.dropdown-footer-list {
font-size: 14px;
@@ -395,3 +402,122 @@
height: 15px;
border-radius: $border-radius-base;
}
+
+.dropdown-menu-due-date {
+ .dropdown-content {
+ max-height: 230px;
+ }
+
+ .ui-widget {
+ table {
+ margin: 0;
+ }
+
+ &.ui-datepicker-inline {
+ padding: 0 10px;
+ border: 0;
+ width: 100%;
+ }
+
+ .ui-datepicker-header {
+ padding: 0 8px 10px;
+ border: 0;
+
+ .ui-icon {
+ background: none;
+ font-size: 20px;
+ text-indent: 0;
+
+ &:before {
+ display: block;
+ position: relative;
+ top: -2px;
+ color: $dropdown-title-btn-color;
+ font: normal normal normal 14px/1 FontAwesome;
+ font-size: inherit;
+ text-rendering: auto;
+ -webkit-font-smoothing: antialiased;
+ -moz-osx-font-smoothing: grayscale;
+ }
+ }
+ }
+
+ .ui-state-active,
+ .ui-state-hover {
+ color: $md-link-color;
+ background-color: $calendar-hover-bg;
+ }
+
+ .ui-datepicker-prev,
+ .ui-datepicker-next {
+ top: 0;
+ height: 15px;
+ cursor: pointer;
+
+ &:hover {
+ background-color: transparent;
+ border: 0;
+
+ .ui-icon:before {
+ color: $md-link-color;
+ }
+ }
+ }
+
+ .ui-datepicker-prev {
+ left: 0;
+
+ .ui-icon:before {
+ content: '\f104';
+ text-align: left;
+ }
+ }
+
+ .ui-datepicker-next {
+ right: 0;
+
+ .ui-icon:before {
+ content: '\f105';
+ text-align: right;
+ }
+ }
+
+ td {
+ padding: 0;
+ border: 1px solid $calendar-border-color;
+
+ &:first-child {
+ border-left: 0;
+ }
+
+ &:last-child {
+ border-right: 0;
+ }
+
+ a {
+ line-height: 17px;
+ border: 0;
+ border-radius: 0;
+ }
+ }
+
+ .ui-datepicker-title {
+ color: $gl-gray;
+ font-size: 15px;
+ line-height: 1;
+ font-weight: normal;
+ }
+ }
+
+ th {
+ padding: 2px 0;
+ color: $calendar-header-color;
+ font-weight: normal;
+ text-transform: lowercase;
+ border-top: 1px solid $calendar-border-color;
+ }
+
+ .ui-datepicker-unselectable {
+ background-color: $calendar-unselectable-bg;
+ }
+}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 0f32d36d59c..704fa1ff800 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -90,3 +90,12 @@
box-shadow: none;
width: 100%;
}
+
+.md {
+ &.md-preview-holder {
+ code {
+ white-space: pre-wrap;
+ word-break: break-all;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index f910cf61817..30ca27ab104 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5;
$note-toolbar-color: #959494;
$zen-control-hover-color: #111;
+
+$calendar-header-color: #b8b8b8;
+$calendar-hover-bg: #ecf3fe;
+$calendar-border-color: rgba(#000, .1);
+$calendar-unselectable-bg: #faf9f9;
diff --git a/app/assets/stylesheets/pages/detail_page.scss b/app/assets/stylesheets/pages/detail_page.scss
index 2c2ac903f29..751a5ab4d92 100644
--- a/app/assets/stylesheets/pages/detail_page.scss
+++ b/app/assets/stylesheets/pages/detail_page.scss
@@ -36,4 +36,10 @@
}
}
}
+
+ .wiki {
+ code {
+ white-space: pre-wrap;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 183f22a1b24..e7c8198ba45 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -34,6 +34,7 @@
background: #fff;
color: #333;
border-radius: 0 0 3px 3px;
+ -webkit-overflow-scrolling: auto;
.unfold {
cursor: pointer;
@@ -86,7 +87,7 @@
}
span {
- white-space: pre;
+ white-space: pre-wrap;
}
}
}
@@ -335,7 +336,7 @@
}
.diff-file .line_content {
- white-space: pre;
+ white-space: pre-wrap;
}
.diff-wrap-lines .line_content {
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 0f0592a0ab8..8981f070a20 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -26,6 +26,10 @@
line-height: 42px;
padding-top: 7px;
padding-bottom: 7px;
+
+ .pull-right {
+ height: 20px;
+ }
}
.editor-ref {
@@ -53,4 +57,9 @@
.select2 {
float: right;
}
+
+ .encoding-selector,
+ .license-selector {
+ display: inline-block;
+ }
}
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 604f1700cf8..ee95bdf488e 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -59,8 +59,10 @@
position: relative;
overflow-y: auto;
padding: 15px;
+
.form-actions {
margin: -$gl-padding+1;
+ margin-top: 15px;
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 5bf44c1cdb6..1d6190c8f18 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -128,6 +128,7 @@
top: 58px;
bottom: 0;
right: 0;
+ z-index: 10;
transition: width .3s;
background: $gray-light;
padding: 10px 20px;
@@ -241,7 +242,7 @@
}
}
- .btn {
+ .issuable-pager {
background: $gray-normal;
border: 1px solid $border-gray-normal;
&:hover {
@@ -250,7 +251,7 @@
}
}
- a:not(.btn) {
+ a:not(.issuable-pager) {
&:hover {
color: $md-link-color;
text-decoration: none;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index c2371d79989..7fa13e66b43 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -42,6 +42,7 @@
.note-textarea {
display: block;
padding: 10px 0;
+ color: $gl-gray;
font-family: $regular_font;
border: 0;
@@ -83,18 +84,6 @@
border-color: $gl-success;
}
}
-
- p {
- code {
- white-space: normal;
- }
-
- pre {
- code {
- white-space: pre;
- }
- }
- }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index ffcd89a3c9f..82f78e8d796 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -81,16 +81,8 @@ ul.notes {
@include md-typography;
// On diffs code should wrap nicely and not overflow
- p {
- code {
- white-space: normal;
- }
-
- pre {
- code {
- white-space: pre;
- }
- }
+ code {
+ white-space: pre-wrap;
}
// Reset ul style types since we're nested inside a ul already
@@ -137,7 +129,7 @@ ul.notes {
margin-right: 10px;
}
.line_content {
- white-space: pre;
+ white-space: pre-wrap;
}
}
@@ -171,11 +163,6 @@ ul.notes {
&.parallel {
border-width: 1px;
-
- .code,
- code {
- white-space: pre-wrap;
- }
}
.notes {
@@ -308,7 +295,7 @@ ul.notes {
padding: 4px;
font-size: 16px;
color: $gl-link-color;
- margin-left: -60px;
+ margin-left: -56px;
position: absolute;
z-index: 10;
width: 32px;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index b4a28b8dd3f..ec22548ddeb 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -75,6 +75,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:admin_notification_email,
:user_oauth_applications,
:shared_runners_enabled,
+ :shared_runners_text,
:max_artifacts_size,
:metrics_enabled,
:metrics_host,
@@ -92,6 +93,7 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
:akismet_api_key,
:email_author_in_body,
:repository_checks_enabled,
+ :metrics_packet_size,
restricted_visibility_levels: [],
import_sources: []
)
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index 0bd19c49d8f..93c4894ea0f 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -39,6 +39,6 @@ class Admin::HooksController < Admin::ApplicationController
end
def hook_params
- params.require(:hook).permit(:url, :enable_ssl_verification)
+ params.require(:hook).permit(:url, :enable_ssl_verification, :push_events, :tag_push_events)
end
end
diff --git a/app/controllers/projects/builds_controller.rb b/app/controllers/projects/builds_controller.rb
index f159e169f6d..b8b9e78427d 100644
--- a/app/controllers/projects/builds_controller.rb
+++ b/app/controllers/projects/builds_controller.rb
@@ -1,7 +1,7 @@
class Projects::BuildsController < Projects::ApplicationController
before_action :build, except: [:index, :cancel_all]
before_action :authorize_read_build!, except: [:cancel, :cancel_all, :retry]
- before_action :authorize_update_build!, except: [:index, :show, :status]
+ before_action :authorize_update_build!, except: [:index, :show, :status, :raw]
layout 'project'
def index
@@ -62,6 +62,14 @@ class Projects::BuildsController < Projects::ApplicationController
notice: "Build has been sucessfully erased!"
end
+ def raw
+ if @build.has_trace?
+ send_file @build.path_to_trace, type: 'text/plain; charset=utf-8', disposition: 'inline'
+ else
+ render_404
+ end
+ end
+
private
def build
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 576fa3cedb2..4d64a2d9884 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -12,7 +12,7 @@ class Projects::CommitController < Projects::ApplicationController
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
before_action :define_show_vars, only: [:show, :builds]
- before_action :authorize_edit_tree!, only: [:revert]
+ before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
def show
apply_diff_view_cookie!
@@ -60,27 +60,32 @@ class Projects::CommitController < Projects::ApplicationController
end
def revert
- assign_revert_commit_vars
+ assign_change_commit_vars(@commit.revert_branch_name)
return render_404 if @target_branch.blank?
- create_commit(Commits::RevertService, success_notice: "The #{revert_type_title} has been successfully reverted.",
- success_path: successful_revert_path, failure_path: failed_revert_path)
+ create_commit(Commits::RevertService, success_notice: "The #{@commit.change_type_title} has been successfully reverted.",
+ success_path: successful_change_path, failure_path: failed_change_path)
end
+
+ def cherry_pick
+ assign_change_commit_vars(@commit.cherry_pick_branch_name)
+
+ return render_404 if @target_branch.blank?
- private
-
- def revert_type_title
- @commit.merged_merge_request ? 'merge request' : 'commit'
+ create_commit(Commits::CherryPickService, success_notice: "The #{@commit.change_type_title} has been successfully cherry-picked.",
+ success_path: successful_change_path, failure_path: failed_change_path)
end
- def successful_revert_path
+ private
+
+ def successful_change_path
return referenced_merge_request_url if @commit.merged_merge_request
namespace_project_commits_url(@project.namespace, @project, @target_branch)
end
- def failed_revert_path
+ def failed_change_path
return referenced_merge_request_url if @commit.merged_merge_request
namespace_project_commit_url(@project.namespace, @project, params[:id])
@@ -111,14 +116,13 @@ class Projects::CommitController < Projects::ApplicationController
@statuses = ci_commit.statuses if ci_commit
end
- def assign_revert_commit_vars
+ def assign_change_commit_vars(mr_source_branch)
@commit = project.commit(params[:id])
@target_branch = params[:target_branch]
- @mr_source_branch = @commit.revert_branch_name
+ @mr_source_branch = mr_source_branch
@mr_target_branch = @target_branch
@commit_params = {
commit: @commit,
- revert_type_title: revert_type_title,
create_merge_request: params[:create_merge_request].present? || different_project?
}
end
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index 4159e53bfa9..606552fa853 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -7,10 +7,12 @@ class Projects::GroupLinksController < Projects::ApplicationController
end
def create
- link = project.project_group_links.new
- link.group_id = params[:link_group_id]
- link.group_access = params[:link_group_access]
- link.save
+ group = Group.find(params[:link_group_id])
+ return render_404 unless can?(current_user, :read_group, group)
+
+ project.project_group_links.create(
+ group: group, group_access: params[:link_group_access]
+ )
redirect_to namespace_project_group_links_path(project.namespace, project)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 38214f04793..b96ab91c17d 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController
end
@issues = @issues.page(params[:page])
- @label = @project.labels.find_by(title: params[:label_name])
+ @labels = @project.labels.where(title: params[:label_name])
respond_to do |format|
format.html
format.atom { render layout: false }
format.json do
render json: {
- html: view_to_html_string("projects/issues/_issues")
+ html: view_to_html_string("projects/issues/_issues"),
+ labels: @labels.as_json(methods: :text_color)
}
end
end
@@ -128,10 +129,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def related_branches
- merge_requests = @issue.referenced_merge_requests(current_user)
-
- @related_branches = @issue.related_branches -
- merge_requests.map(&:source_branch)
+ @related_branches = @issue.related_branches(current_user)
respond_to do |format|
format.json do
@@ -194,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
def issue_params
params.require(:issue).permit(
:title, :assignee_id, :position, :description, :confidential,
- :milestone_id, :state_event, :task_num, label_ids: []
+ :milestone_id, :due_date, :state_event, :task_num, label_ids: []
)
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 3e0cfc6aa65..1388ea9d66c 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests = @merge_requests.page(params[:page])
@merge_requests = @merge_requests.preload(:target_project)
- @label = @project.labels.find_by(title: params[:label_name])
+ @labels = @project.labels.where(title: params[:label_name])
respond_to do |format|
format.html
format.json do
render json: {
- html: view_to_html_string("projects/merge_requests/_merge_requests")
+ html: view_to_html_string("projects/merge_requests/_merge_requests"),
+ labels: @labels.as_json(methods: :text_color)
}
end
end
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index 8b2577aebe1..739681f4085 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -6,7 +6,7 @@ class Projects::ServicesController < Projects::ApplicationController
:description, :issues_url, :new_issue_url, :restrict_to_branch, :channel,
:colorize_messages, :channels,
:push_events, :issues_events, :merge_requests_events, :tag_push_events,
- :note_events, :build_events,
+ :note_events, :build_events, :wiki_page_events,
:notify_only_broken_builds, :add_pusher,
:send_from_committer_email, :disable_diffs, :external_wiki_url,
:notify, :color,
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index 9f3a4a69721..c02bc28acef 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -44,7 +44,7 @@ class Projects::WikisController < Projects::ApplicationController
return render('empty') unless can?(current_user, :create_wiki, @project)
- if @page.update(content, format, message)
+ if @page = WikiPages::UpdateService.new(@project, current_user, wiki_params).execute(@page)
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.'
@@ -55,9 +55,9 @@ class Projects::WikisController < Projects::ApplicationController
end
def create
- @page = WikiPage.new(@project_wiki)
+ @page = WikiPages::CreateService.new(@project, current_user, wiki_params).execute
- if @page.create(wiki_params)
+ if @page.persisted?
redirect_to(
namespace_project_wiki_path(@project.namespace, @project, @page),
notice: 'Wiki was successfully updated.'
@@ -122,15 +122,4 @@ class Projects::WikisController < Projects::ApplicationController
params[:wiki].slice(:title, :content, :format, :message)
end
- def content
- params[:wiki][:content]
- end
-
- def format
- params[:wiki][:format]
- end
-
- def message
- params[:wiki][:message]
- end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index ca71acb65d3..93aa30b3255 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -39,6 +39,7 @@ class IssuableFinder
items = by_assignee(items)
items = by_author(items)
items = by_label(items)
+ items = by_due_date(items)
sort(items)
end
@@ -117,7 +118,7 @@ class IssuableFinder
end
def filter_by_no_label?
- labels? && params[:label_name] == Label::None.title
+ labels? && params[:label_name].include?(Label::None.title)
end
def labels
@@ -277,9 +278,47 @@ class IssuableFinder
end
end
+ # When filtering by multiple labels we may end up duplicating issues (if one
+ # has multiple labels). This ensures we only return unique issues.
+ items.distinct
+ end
+
+ def by_due_date(items)
+ if due_date?
+ if filter_by_no_due_date?
+ items = items.without_due_date
+ elsif filter_by_overdue?
+ items = items.due_before(Date.today)
+ elsif filter_by_due_this_week?
+ items = items.due_between(Date.today.beginning_of_week, Date.today.end_of_week)
+ elsif filter_by_due_this_month?
+ items = items.due_between(Date.today.beginning_of_month, Date.today.end_of_month)
+ end
+ end
+
items
end
+ def filter_by_no_due_date?
+ due_date? && params[:due_date] == Issue::NoDueDate.name
+ end
+
+ def filter_by_overdue?
+ due_date? && params[:due_date] == Issue::Overdue.name
+ end
+
+ def filter_by_due_this_week?
+ due_date? && params[:due_date] == Issue::DueThisWeek.name
+ end
+
+ def filter_by_due_this_month?
+ due_date? && params[:due_date] == Issue::DueThisMonth.name
+ end
+
+ def due_date?
+ params[:due_date].present? && klass.column_names.include?('due_date')
+ end
+
def label_names
params[:label_name].is_a?(String) ? params[:label_name].split(',') : params[:label_name]
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 16e5b8ac223..3e0074da394 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -254,11 +254,11 @@ module ApplicationHelper
def page_filter_path(options = {})
without = options.delete(:without)
+ add_label = options.delete(:label)
exist_opts = {
state: params[:state],
scope: params[:scope],
- label_name: params[:label_name],
milestone_title: params[:milestone_title],
assignee_id: params[:assignee_id],
author_id: params[:author_id],
@@ -275,6 +275,13 @@ module ApplicationHelper
path = request.path
path << "?#{options.to_param}"
+ if add_label
+ if params[:label_name].present? and params[:label_name].respond_to?('any?')
+ params[:label_name].each do |label|
+ path << "&label_name[]=#{label}"
+ end
+ end
+ end
path
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 60a0ff32c9c..914b0ef6042 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -15,6 +15,10 @@ module ApplicationSettingsHelper
current_application_settings.sign_in_text
end
+ def shared_runners_text
+ current_application_settings.shared_runners_text
+ end
+
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 9e59a295fc4..a4d7c425d0f 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -173,4 +173,15 @@ module BlobHelper
response.etag = @blob.id
!stale
end
+
+ def licenses_for_select
+ return @licenses_for_select if defined?(@licenses_for_select)
+
+ licenses = Licensee::License.all
+
+ @licenses_for_select = {
+ Popular: licenses.select(&:featured).map { |license| [license.name, license.key] },
+ Other: licenses.reject(&:featured).map { |license| [license.name, license.key] }
+ }
+ end
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 35ba543cef1..b59c3982edd 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -126,12 +126,10 @@ module CommitsHelper
def revert_commit_link(commit, continue_to_path, btn_class: nil)
return unless current_user
- tooltip = "Revert this #{revert_commit_type(commit)} in a new merge request"
+ tooltip = "Revert this #{commit.change_type_title} in a new merge request"
if can_collaborate_with_project?
- content_tag :span, 'data-toggle' => 'modal', 'data-target' => '#modal-revert-commit' do
- link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class}"
- end
+ link_to 'Revert', '#modal-revert-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip"
elsif can?(current_user, :fork_project, @project)
continue_params = {
to: continue_to_path,
@@ -146,11 +144,24 @@ module CommitsHelper
end
end
- def revert_commit_type(commit)
- if commit.merged_merge_request
- 'merge request'
- else
- 'commit'
+ def cherry_pick_commit_link(commit, continue_to_path, btn_class: nil)
+ return unless current_user
+
+ tooltip = "Cherry-pick this #{commit.change_type_title} in a new merge request"
+
+ if can_collaborate_with_project?
+ link_to 'Cherry-pick', '#modal-cherry-pick-commit', 'data-toggle' => 'modal', 'data-container' => 'body', title: tooltip, class: "btn btn-default btn-grouped btn-#{btn_class} has-tooltip"
+ elsif can?(current_user, :fork_project, @project)
+ continue_params = {
+ to: continue_to_path,
+ notice: edit_in_new_fork_notice + ' Try to cherry-pick this commit again.',
+ notice_now: edit_in_new_fork_notice_now
+ }
+ fork_path = namespace_project_forks_path(@project.namespace, @project,
+ namespace_key: current_user.namespace.id,
+ continue: continue_params)
+
+ link_to 'Cherry-pick', fork_path, class: 'btn btn-grouped btn-close', method: :post, 'data-toggle' => 'tooltip', 'data-container' => 'body', title: tooltip
end
end
@@ -183,7 +194,7 @@ module CommitsHelper
options = {
class: "commit-#{options[:source]}-link has-tooltip",
- data: { 'original-title'.to_sym => sanitize(source_email) }
+ title: source_email
}
if user.nil?
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
new file mode 100644
index 00000000000..109bc1a02d1
--- /dev/null
+++ b/app/helpers/import_helper.rb
@@ -0,0 +1,18 @@
+module ImportHelper
+ def github_project_link(path_with_namespace)
+ link_to path_with_namespace, github_project_url(path_with_namespace), target: '_blank'
+ end
+
+ private
+
+ def github_project_url(path_with_namespace)
+ "#{github_root_url}/#{path_with_namespace}"
+ end
+
+ def github_root_url
+ return @github_url if defined?(@github_url)
+
+ provider = Gitlab.config.omniauth.providers.find { |p| p.name == 'github' }
+ @github_url = provider.fetch('url', 'https://github.com') if provider
+ end
+end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 49b6b79ce35..39474217286 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -16,6 +16,25 @@ module IssuablesHelper
base_issuable_scope(issuable).where('iid > ?', issuable.iid).last
end
+ def multi_label_name(current_labels, default_label)
+ # current_labels may be a string from before
+ if current_labels.is_a?(Array)
+ if current_labels.count > 1
+ "#{current_labels[0]} +#{current_labels.count - 1} more"
+ else
+ current_labels[0]
+ end
+ elsif current_labels.is_a?(String)
+ if current_labels.nil? || current_labels.empty?
+ default_label
+ else
+ current_labels
+ end
+ else
+ default_label
+ end
+ end
+
def issuable_json_path(issuable)
project = issuable.project
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 4cb8adcebad..afe1e11a0da 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -131,7 +131,7 @@ module IssuesHelper
class: "icon emoji-icon emoji-#{unicode}",
title: name,
data: data
- else
+ else
# Emoji icons displayed separately, used for the awards already given
# to an issue or merge request.
content_tag :img, "",
@@ -172,6 +172,18 @@ module IssuesHelper
end.to_h
end
+ def due_date_options
+ options = [
+ Issue::AnyDueDate,
+ Issue::NoDueDate,
+ Issue::DueThisWeek,
+ Issue::DueThisMonth,
+ Issue::Overdue
+ ]
+
+ options_from_collection_for_select(options, 'name', 'title', params[:due_date])
+ end
+
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 7e00aacceaa..2f164da326c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -52,7 +52,7 @@ module ProjectsHelper
link_to(author_html, user_path(author), class: "author_link #{"#{opts[:mobile_classes]}" if opts[:mobile_classes]}").html_safe
else
title = opts[:title].sub(":name", sanitize(author.name))
- link_to(author_html, user_path(author), class: "author_link has-tooltip", data: { 'original-title'.to_sym => title, container: 'body' } ).html_safe
+ link_to(author_html, user_path(author), class: "author_link has-tooltip", title: title, data: { container: 'body' } ).html_safe
end
end
@@ -216,40 +216,14 @@ module ProjectsHelper
end
end
- def add_contribution_guide_path(project)
- if project && !project.repository.contribution_guide
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "CONTRIBUTING.md",
- commit_message: "Add contribution guide"
- )
- end
- end
-
- def add_changelog_path(project)
- if project && !project.repository.changelog
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "CHANGELOG",
- commit_message: "Add changelog"
- )
- end
- end
-
- def add_license_path(project)
- if project && !project.repository.license
- namespace_project_new_blob_path(
- project.namespace,
- project,
- project.default_branch,
- file_name: "LICENSE",
- commit_message: "Add license"
- )
- end
+ def add_special_file_path(project, file_name:, commit_message: nil)
+ namespace_project_new_blob_path(
+ project.namespace,
+ project,
+ project.default_branch || 'master',
+ file_name: file_name,
+ commit_message: commit_message || "Add #{file_name.downcase}"
+ )
end
def contribution_guide_path(project)
@@ -272,7 +246,7 @@ module ProjectsHelper
end
def license_path(project)
- filename_path(project, :license)
+ filename_path(project, :license_blob)
end
def version_path(project)
@@ -306,6 +280,13 @@ module ProjectsHelper
namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'README.md')
end
+ def new_license_path
+ ref = @repository.root_ref if @repository
+ ref ||= 'master'
+
+ namespace_project_new_blob_path(@project.namespace, @project, tree_join(ref), file_name: 'LICENSE')
+ end
+
def last_push_event
if current_user
current_user.recent_push(@project.id)
@@ -335,6 +316,12 @@ module ProjectsHelper
@ref || @repository.try(:root_ref)
end
+ def license_short_name(project)
+ license = Licensee::License.new(project.repository.license_key)
+
+ license.nickname || license.name
+ end
+
private
def filename_path(project, filename)
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 4fc6de59a8b..e951a87a212 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -2,32 +2,29 @@ module SelectsHelper
def users_select_tag(id, opts = {})
css_class = "ajax-users-select "
css_class << "multiselect " if opts[:multiple]
+ css_class << "skip_ldap " if opts[:skip_ldap]
css_class << (opts[:class] || '')
value = opts[:selected] || ''
- placeholder = opts[:placeholder] || 'Search for a user'
- null_user = opts[:null_user] || false
- any_user = opts[:any_user] || false
- email_user = opts[:email_user] || false
- first_user = opts[:first_user] && current_user ? current_user.username : false
- current_user = opts[:current_user] || false
- author_id = opts[:author_id] || ''
- project = opts[:project] || @project
+ first_user = opts[:first_user] && current_user ? current_user.username : false
html = {
class: css_class,
data: {
- placeholder: placeholder,
- null_user: null_user,
- any_user: any_user,
- email_user: email_user,
+ placeholder: opts[:placeholder] || 'Search for a user',
+ null_user: opts[:null_user] || false,
+ any_user: opts[:any_user] || false,
+ email_user: opts[:email_user] || false,
first_user: first_user,
- current_user: current_user,
- author_id: author_id
+ current_user: opts[:current_user] || false,
+ "push-code-to-protected-branches" => opts[:push_code_to_protected_branches],
+ author_id: opts[:author_id] || ''
}
}
unless opts[:scope] == :all
+ project = opts[:project] || @project
+
if project
html['data-project-id'] = project.id
elsif @group
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index 2f2d2721d6d..630e10ea892 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -8,6 +8,8 @@ module SortingHelper
sort_value_oldest_created => sort_title_oldest_created,
sort_value_milestone_soon => sort_title_milestone_soon,
sort_value_milestone_later => sort_title_milestone_later,
+ sort_value_due_date_soon => sort_title_due_date_soon,
+ sort_value_due_date_later => sort_title_due_date_later,
sort_value_largest_repo => sort_title_largest_repo,
sort_value_recently_signin => sort_title_recently_signin,
sort_value_oldest_signin => sort_title_oldest_signin,
@@ -50,6 +52,14 @@ module SortingHelper
'Milestone due later'
end
+ def sort_title_due_date_soon
+ 'Due soon'
+ end
+
+ def sort_title_due_date_later
+ 'Due later'
+ end
+
def sort_title_name
'Name'
end
@@ -98,6 +108,14 @@ module SortingHelper
'milestone_due_desc'
end
+ def sort_value_due_date_soon
+ 'due_date_asc'
+ end
+
+ def sort_value_due_date_later
+ 'due_date_desc'
+ end
+
def sort_value_name
'name_asc'
end
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index 4920ca5af6e..dbedf417fa5 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -66,7 +66,7 @@ module TreeHelper
ref
else
project = tree_edit_project(project)
- project.repository.next_patch_branch
+ project.repository.next_branch('patch')
end
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 55bb4f65270..9dd11d20ea6 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -56,7 +56,7 @@ module Emails
{
from: sender(sender_id),
to: recipient(recipient_id),
- subject: subject("#{@merge_request.title} (##{@merge_request.iid})")
+ subject: subject("#{@merge_request.title} (#{@merge_request.to_reference})")
}
end
end
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index f9650df9a74..cdc40b81ee1 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -38,7 +38,7 @@ module Emails
{
from: sender(@note.author_id),
to: recipient(recipient_id),
- subject: subject("#{@note.noteable.title} (##{@note.noteable.iid})")
+ subject: subject("#{@note.noteable.title} (#{@note.noteable.to_reference})")
}
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 85ef0523b31..d42a65620ff 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -230,12 +230,33 @@ module Ci
end
end
+ def trace_length
+ if raw_trace
+ raw_trace.length
+ else
+ 0
+ end
+ end
+
def trace=(trace)
+ recreate_trace_dir
+ File.write(path_to_trace, trace)
+ end
+
+ def recreate_trace_dir
unless Dir.exists?(dir_to_trace)
FileUtils.mkdir_p(dir_to_trace)
end
+ end
+ private :recreate_trace_dir
- File.write(path_to_trace, trace)
+ def append_trace(trace_part, offset)
+ recreate_trace_dir
+
+ File.truncate(path_to_trace, offset) if File.exist?(path_to_trace)
+ File.open(path_to_trace, 'a') do |f|
+ f.write(trace_part)
+ end
end
def dir_to_trace
diff --git a/app/models/commit.rb b/app/models/commit.rb
index d1f07ccd55c..6bb018b086f 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -15,8 +15,8 @@ class Commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
# Commits above this size will not be rendered in HTML
- DIFF_HARD_LIMIT_FILES = 1000 unless defined?(DIFF_HARD_LIMIT_FILES)
- DIFF_HARD_LIMIT_LINES = 50000 unless defined?(DIFF_HARD_LIMIT_LINES)
+ DIFF_HARD_LIMIT_FILES = 1000
+ DIFF_HARD_LIMIT_LINES = 50000
class << self
def decorate(commits, project)
@@ -218,6 +218,10 @@ class Commit
def revert_branch_name
"revert-#{short_id}"
end
+
+ def cherry_pick_branch_name
+ project.repository.next_branch("cherry-pick-#{short_id}", mild: true)
+ end
def revert_description
if merged_merge_request
@@ -253,6 +257,10 @@ class Commit
end.any? { |commit_ref| commit_ref.reverts_commit?(self) }
end
+ def change_type_title
+ merged_merge_request ? 'merge request' : 'commit'
+ end
+
private
def repo_changes
diff --git a/app/models/group.rb b/app/models/group.rb
index 9a04ac70d35..1f8432e3320 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -15,7 +15,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class Group < Namespace
include Gitlab::ConfigHelper
diff --git a/app/models/hooks/project_hook.rb b/app/models/hooks/project_hook.rb
index fe923fafbe0..bc6e0f98c3c 100644
--- a/app/models/hooks/project_hook.rb
+++ b/app/models/hooks/project_hook.rb
@@ -21,10 +21,9 @@
class ProjectHook < WebHook
belongs_to :project
- scope :push_hooks, -> { where(push_events: true) }
- scope :tag_push_hooks, -> { where(tag_push_events: true) }
scope :issue_hooks, -> { where(issues_events: true) }
scope :note_hooks, -> { where(note_events: true) }
scope :merge_request_hooks, -> { where(merge_requests_events: true) }
scope :build_hooks, -> { where(build_events: true) }
+ scope :wiki_page_hooks, -> { where(wiki_page_events: true) }
end
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index c147d8762a9..15dddcc2447 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -19,4 +19,7 @@
#
class SystemHook < WebHook
+ def async_execute(data, hook_name)
+ Sidekiq::Client.enqueue(SystemHookWorker, id, data, hook_name)
+ end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index 7a13c3f0a39..3a2e4f546f7 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -30,6 +30,9 @@ class WebHook < ActiveRecord::Base
default_value_for :build_events, false
default_value_for :enable_ssl_verification, true
+ scope :push_hooks, -> { where(push_events: true) }
+ scope :tag_push_hooks, -> { where(tag_push_events: true) }
+
# HTTParty timeout
default_timeout Gitlab.config.gitlab.webhook_timeout
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 3f188e04770..ea1bfb776ee 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -20,7 +20,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class Issue < ActiveRecord::Base
include InternalId
@@ -29,6 +28,13 @@ class Issue < ActiveRecord::Base
include Sortable
include Taskable
+ DueDateStruct = Struct.new(:title, :name).freeze
+ NoDueDate = DueDateStruct.new('No Due Date', '0').freeze
+ AnyDueDate = DueDateStruct.new('Any Due Date', '').freeze
+ Overdue = DueDateStruct.new('Overdue', 'overdue').freeze
+ DueThisWeek = DueDateStruct.new('Due This Week', 'week').freeze
+ DueThisMonth = DueDateStruct.new('Due This Month', 'month').freeze
+
ActsAsTaggableOn.strict_case_match = true
belongs_to :project
@@ -40,6 +46,13 @@ class Issue < ActiveRecord::Base
scope :open_for, ->(user) { opened.assigned_to(user) }
scope :in_projects, ->(project_ids) { where(project_id: project_ids) }
+ scope :without_due_date, -> { where(due_date: nil) }
+ scope :due_before, ->(date) { where('issues.due_date < ?', date) }
+ scope :due_between, ->(from_date, to_date) { where('issues.due_date >= ?', from_date).where('issues.due_date <= ?', to_date) }
+
+ scope :order_due_date_asc, -> { reorder('issues.due_date IS NULL, issues.due_date ASC') }
+ scope :order_due_date_desc, -> { reorder('issues.due_date IS NULL, issues.due_date DESC') }
+
state_machine :state, initial: :opened do
event :close do
transition [:reopened, :opened] => :closed
@@ -83,6 +96,15 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
+ def self.sort(method)
+ case method.to_s
+ when 'due_date_asc' then order_due_date_asc
+ when 'due_date_desc' then order_due_date_desc
+ else
+ super
+ end
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -104,10 +126,16 @@ class Issue < ActiveRecord::Base
end
end
- def related_branches
- project.repository.branch_names.select do |branch|
+ # All branches containing the current issue's ID, except for
+ # those with a merge request open referencing the current issue.
+ def related_branches(current_user)
+ branches_with_iid = project.repository.branch_names.select do |branch|
branch =~ /\A#{iid}-(?!\d+-stable)/i
end
+
+ branches_with_merge_request = self.referenced_merge_requests(current_user).map(&:source_branch)
+
+ branches_with_iid - branches_with_merge_request
end
# Reset issue events cache
@@ -151,13 +179,21 @@ class Issue < ActiveRecord::Base
end
def to_branch_name
- "#{iid}-#{title.parameterize}"
+ if self.confidential?
+ "#{iid}-confidential-issue"
+ else
+ "#{iid}-#{title.parameterize}"
+ end
end
def can_be_worked_on?(current_user)
!self.closed? &&
!self.project.forked? &&
- self.related_branches.empty? &&
+ self.related_branches(current_user).empty? &&
self.closed_by_merge_requests(current_user).empty?
end
+
+ def overdue?
+ due_date.try(:past?) || false
+ end
end
diff --git a/app/models/label.rb b/app/models/label.rb
index 55c01cae762..60bdce32952 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base
template
end
+ def text_color
+ LabelsHelper::text_color_for_bg(self.color)
+ end
+
private
def label_format_reference(format = :id)
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e410febdfff..dbecc48485c 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -27,9 +27,6 @@
# merge_commit_sha :string
#
-require Rails.root.join("app/models/commit")
-require Rails.root.join("lib/static_model")
-
class MergeRequest < ActiveRecord::Base
include InternalId
include Issuable
@@ -605,4 +602,8 @@ class MergeRequest < ActiveRecord::Base
def can_be_reverted?(current_user = nil)
merge_commit && !merge_commit.has_been_reverted?(current_user, self)
end
+
+ def can_be_cherry_picked?
+ merge_commit
+ end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 33884118595..0580cafdd1b 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -11,8 +11,6 @@
# updated_at :datetime
#
-require Rails.root.join("app/models/commit")
-
class MergeRequestDiff < ActiveRecord::Base
include Sortable
diff --git a/app/models/note.rb b/app/models/note.rb
index 87ced65c650..71b4293d57a 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -20,7 +20,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class Note < ActiveRecord::Base
include Gitlab::CurrentSettings
diff --git a/app/models/project.rb b/app/models/project.rb
index 8f20922e3c5..8f0272d2ce0 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -40,7 +40,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class Project < ActiveRecord::Base
include Gitlab::ConfigHelper
@@ -831,8 +830,8 @@ class Project < ActiveRecord::Base
end
end
- def hook_attrs
- {
+ def hook_attrs(backward: true)
+ attrs = {
name: name,
description: description,
web_url: web_url,
@@ -843,12 +842,19 @@ class Project < ActiveRecord::Base
visibility_level: visibility_level,
path_with_namespace: path_with_namespace,
default_branch: default_branch,
- # Backward compatibility
- homepage: web_url,
- url: url_to_repo,
- ssh_url: ssh_url_to_repo,
- http_url: http_url_to_repo
}
+
+ # Backward compatibility
+ if backward
+ attrs.merge!({
+ homepage: web_url,
+ url: url_to_repo,
+ ssh_url: ssh_url_to_repo,
+ http_url: http_url_to_repo
+ })
+ end
+
+ attrs
end
# Reset events cache related to this project
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 79efb403058..2c0ae312f1b 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -8,7 +8,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class ProjectImportData < ActiveRecord::Base
belongs_to :project
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index 0e3fa4a40fe..064ef8e8674 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -183,7 +183,7 @@ class HipchatService < Service
title = obj_attr[:title]
merge_request_url = "#{project_url}/merge_requests/#{merge_request_id}"
- merge_request_link = "<a href=\"#{merge_request_url}\">merge request ##{merge_request_id}</a>"
+ merge_request_link = "<a href=\"#{merge_request_url}\">merge request !#{merge_request_id}</a>"
message = "#{user_name} #{state} #{merge_request_link} in " \
"#{project_link}: <b>#{title}</b>"
@@ -224,7 +224,7 @@ class HipchatService < Service
when "MergeRequest"
subj_attr = HashWithIndifferentAccess.new(data[:merge_request])
subject_id = subj_attr[:iid]
- subject_desc = "##{subject_id}"
+ subject_desc = "!#{subject_id}"
subject_type = "merge request"
title = format_title(subj_attr[:title])
when "Snippet"
diff --git a/app/models/project_services/slack_service.rb b/app/models/project_services/slack_service.rb
index d89cf6d17b2..fd65027f084 100644
--- a/app/models/project_services/slack_service.rb
+++ b/app/models/project_services/slack_service.rb
@@ -60,7 +60,7 @@ class SlackService < Service
end
def supported_events
- %w(push issue merge_request note tag_push build)
+ %w(push issue merge_request note tag_push build wiki_page)
end
def execute(data)
@@ -90,6 +90,8 @@ class SlackService < Service
NoteMessage.new(data)
when "build"
BuildMessage.new(data) if should_build_be_notified?(data)
+ when "wiki_page"
+ WikiPageMessage.new(data)
end
opt = {}
@@ -133,3 +135,4 @@ require "slack_service/push_message"
require "slack_service/merge_message"
require "slack_service/note_message"
require "slack_service/build_message"
+require "slack_service/wiki_page_message"
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb
index e792c258f73..11fc691022b 100644
--- a/app/models/project_services/slack_service/merge_message.rb
+++ b/app/models/project_services/slack_service/merge_message.rb
@@ -50,7 +50,7 @@ class SlackService
end
def merge_request_link
- "[merge request ##{merge_request_id}](#{merge_request_url})"
+ "[merge request !#{merge_request_id}](#{merge_request_url})"
end
def merge_request_url
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index b15d9a14677..89ba51cb662 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -58,7 +58,7 @@ class SlackService
def create_merge_note(merge_request)
commented_on_message(
- "[merge request ##{merge_request[:iid]}](#{@note_url})",
+ "[merge request !#{merge_request[:iid]}](#{@note_url})",
format_title(merge_request[:title]))
end
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb
new file mode 100644
index 00000000000..f336d9e7691
--- /dev/null
+++ b/app/models/project_services/slack_service/wiki_page_message.rb
@@ -0,0 +1,53 @@
+class SlackService
+ class WikiPageMessage < BaseMessage
+ attr_reader :user_name
+ attr_reader :title
+ attr_reader :project_name
+ attr_reader :project_url
+ attr_reader :wiki_page_url
+ attr_reader :action
+ attr_reader :description
+
+ def initialize(params)
+ @user_name = params[:user][:name]
+ @project_name = params[:project_name]
+ @project_url = params[:project_url]
+
+ obj_attr = params[:object_attributes]
+ obj_attr = HashWithIndifferentAccess.new(obj_attr)
+ @title = obj_attr[:title]
+ @wiki_page_url = obj_attr[:url]
+ @description = obj_attr[:content]
+
+ @action =
+ case obj_attr[:action]
+ when "create"
+ "created"
+ when "update"
+ "edited"
+ end
+ end
+
+ def attachments
+ description_message
+ end
+
+ private
+
+ def message
+ "#{user_name} #{action} #{wiki_page_link} in #{project_link}: *#{title}*"
+ end
+
+ def description_message
+ [{ text: format(@description), color: attachment_color }]
+ end
+
+ def project_link
+ "[#{project_name}](#{project_url})"
+ end
+
+ def wiki_page_link
+ "[wiki page](#{wiki_page_url})"
+ end
+ end
+end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 589756f8531..da751591103 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -228,7 +228,8 @@ class Repository
def cache_keys
%i(size branch_names tag_names commit_count
- readme version contribution_guide changelog license)
+ readme version contribution_guide changelog
+ license_blob license_key)
end
def build_cache
@@ -461,27 +462,21 @@ class Repository
end
end
- def license
- cache.fetch(:license) do
- licenses = tree(:head).blobs.find_all do |file|
- file.name =~ /\A(copying|license|licence)/i
- end
-
- preferences = [
- /\Alicen[sc]e\z/i, # LICENSE, LICENCE
- /\Alicen[sc]e\./i, # LICENSE.md, LICENSE.txt
- /\Acopying\z/i, # COPYING
- /\Acopying\.(?!lesser)/i, # COPYING.txt
- /Acopying.lesser/i # COPYING.LESSER
- ]
+ def license_blob
+ return nil if !exists? || empty?
- license = nil
- preferences.each do |r|
- license = licenses.find { |l| l.name =~ r }
- break if license
+ cache.fetch(:license_blob) do
+ if licensee_project.license
+ blob_at_branch(root_ref, licensee_project.matched_file.filename)
end
+ end
+ end
- license
+ def license_key
+ return nil if !exists? || empty?
+
+ cache.fetch(:license_key) do
+ licensee_project.license.try(:key) || 'no-license'
end
end
@@ -549,15 +544,18 @@ class Repository
commit(sha)
end
- def next_patch_branch
- patch_branch_ids = self.branch_names.map do |n|
- result = n.match(/\Apatch-([0-9]+)\z/)
+ def next_branch(name, opts={})
+ branch_ids = self.branch_names.map do |n|
+ next 1 if n == name
+ result = n.match(/\A#{name}-([0-9]+)\z/)
result[1].to_i if result
end.compact
- highest_patch_branch_id = patch_branch_ids.max || 0
+ highest_branch_id = branch_ids.max || 0
- "patch-#{highest_patch_branch_id + 1}"
+ return name if opts[:mild] && 0 == highest_branch_id
+
+ "#{name}-#{highest_branch_id + 1}"
end
# Remove archives older than 2 hours
@@ -760,6 +758,28 @@ class Repository
end
end
+ def cherry_pick(user, commit, base_branch, cherry_pick_tree_id = nil)
+ source_sha = find_branch(base_branch).target
+ cherry_pick_tree_id ||= check_cherry_pick_content(commit, base_branch)
+
+ return false unless cherry_pick_tree_id
+
+ commit_with_hooks(user, base_branch) do |ref|
+ committer = user_to_committer(user)
+ source_sha = Rugged::Commit.create(rugged,
+ message: commit.message,
+ author: {
+ email: commit.author_email,
+ name: commit.author_name,
+ time: commit.authored_date
+ },
+ committer: committer,
+ tree: cherry_pick_tree_id,
+ parents: [rugged.lookup(source_sha)],
+ update_ref: ref)
+ end
+ end
+
def check_revert_content(commit, base_branch)
source_sha = find_branch(base_branch).target
args = [commit.id, source_sha]
@@ -774,6 +794,20 @@ class Repository
tree_id
end
+ def check_cherry_pick_content(commit, base_branch)
+ source_sha = find_branch(base_branch).target
+ args = [commit.id, source_sha]
+ args << 1 if commit.merge_commit?
+
+ cherry_pick_index = rugged.cherrypick_commit(*args)
+ return false if cherry_pick_index.conflicts?
+
+ tree_id = cherry_pick_index.write_tree(rugged)
+ return false unless diff_exists?(source_sha, tree_id)
+
+ tree_id
+ end
+
def diff_exists?(sha1, sha2)
rugged.diff(sha1, sha2).size > 0
end
@@ -925,4 +959,8 @@ class Repository
def cache
@cache ||= RepositoryCache.new(path_with_namespace)
end
+
+ def licensee_project
+ @licensee_project ||= Licensee.project(path)
+ end
end
diff --git a/app/models/service.rb b/app/models/service.rb
index 721273250ea..2645b8321d7 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -32,6 +32,7 @@ class Service < ActiveRecord::Base
default_value_for :tag_push_events, true
default_value_for :note_events, true
default_value_for :build_events, true
+ default_value_for :wiki_page_events, true
after_initialize :initialize_properties
@@ -53,6 +54,7 @@ class Service < ActiveRecord::Base
scope :merge_request_hooks, -> { where(merge_requests_events: true, active: true) }
scope :note_hooks, -> { where(note_events: true, active: true) }
scope :build_hooks, -> { where(build_events: true, active: true) }
+ scope :wiki_page_hooks, -> { where(wiki_page_events: true, active: true) }
default_value_for :category, 'common'
@@ -94,7 +96,7 @@ class Service < ActiveRecord::Base
end
def supported_events
- %w(push tag_push issue merge_request)
+ %w(push tag_push issue merge_request wiki_page)
end
def execute(data)
diff --git a/app/models/user.rb b/app/models/user.rb
index 031315debd7..ab48f8f1960 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -63,7 +63,6 @@
#
require 'carrierwave/orm/activerecord'
-require 'file_size_validator'
class User < ActiveRecord::Base
extend Gitlab::ConfigHelper
diff --git a/app/models/wiki_page.rb b/app/models/wiki_page.rb
index 526760779a4..3d5fd9d3ee9 100644
--- a/app/models/wiki_page.rb
+++ b/app/models/wiki_page.rb
@@ -29,6 +29,10 @@ class WikiPage
# new Page values before writing to the Gollum repository.
attr_accessor :attributes
+ def hook_attrs
+ attributes
+ end
+
def initialize(wiki, page = nil, persisted = false)
@wiki = wiki
@page = page
diff --git a/app/services/commits/change_service.rb b/app/services/commits/change_service.rb
new file mode 100644
index 00000000000..6b69cb53b2c
--- /dev/null
+++ b/app/services/commits/change_service.rb
@@ -0,0 +1,47 @@
+module Commits
+ class ChangeService < ::BaseService
+ class ValidationError < StandardError; end
+ class ChangeError < StandardError; end
+
+ def execute
+ @source_project = params[:source_project] || @project
+ @target_branch = params[:target_branch]
+ @commit = params[:commit]
+ @create_merge_request = params[:create_merge_request].present?
+
+ check_push_permissions unless @create_merge_request
+ commit
+ rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
+ ValidationError, ChangeError => ex
+ error(ex.message)
+ end
+
+ def commit
+ raise NotImplementedError
+ end
+
+ private
+
+ def check_push_permissions
+ allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
+
+ unless allowed
+ raise ValidationError.new('You are not allowed to push into this branch')
+ end
+
+ true
+ end
+
+ def create_target_branch(new_branch)
+ # Temporary branch exists and contains the change commit
+ return success if repository.find_branch(new_branch)
+
+ result = CreateBranchService.new(@project, current_user)
+ .execute(new_branch, @target_branch, source_project: @source_project)
+
+ if result[:status] == :error
+ raise ChangeError, "There was an error creating the source branch: #{result[:message]}"
+ end
+ end
+ end
+end
diff --git a/app/services/commits/cherry_pick_service.rb b/app/services/commits/cherry_pick_service.rb
new file mode 100644
index 00000000000..f9a4efa7182
--- /dev/null
+++ b/app/services/commits/cherry_pick_service.rb
@@ -0,0 +1,19 @@
+module Commits
+ class CherryPickService < ChangeService
+ def commit
+ cherry_pick_into = @create_merge_request ? @commit.cherry_pick_branch_name : @target_branch
+ cherry_pick_tree_id = repository.check_cherry_pick_content(@commit, @target_branch)
+
+ if cherry_pick_tree_id
+ create_target_branch(cherry_pick_into) if @create_merge_request
+
+ repository.cherry_pick(current_user, @commit, cherry_pick_into, cherry_pick_tree_id)
+ success
+ else
+ error_msg = "Sorry, we cannot cherry-pick this #{@commit.change_type_title} automatically.
+ It may have already been cherry-picked, or a more recent commit may have updated some of its content."
+ raise ChangeError, error_msg
+ end
+ end
+ end
+end
diff --git a/app/services/commits/revert_service.rb b/app/services/commits/revert_service.rb
index a3c950ede1f..c7de9f6f35e 100644
--- a/app/services/commits/revert_service.rb
+++ b/app/services/commits/revert_service.rb
@@ -1,21 +1,5 @@
module Commits
- class RevertService < ::BaseService
- class ValidationError < StandardError; end
- class ReversionError < StandardError; end
-
- def execute
- @source_project = params[:source_project] || @project
- @target_branch = params[:target_branch]
- @commit = params[:commit]
- @create_merge_request = params[:create_merge_request].present?
-
- check_push_permissions unless @create_merge_request
- commit
- rescue Repository::CommitError, Gitlab::Git::Repository::InvalidBlobName, GitHooksService::PreReceiveError,
- ValidationError, ReversionError => ex
- error(ex.message)
- end
-
+ class RevertService < ChangeService
def commit
revert_into = @create_merge_request ? @commit.revert_branch_name : @target_branch
revert_tree_id = repository.check_revert_content(@commit, @target_branch)
@@ -26,34 +10,10 @@ module Commits
repository.revert(current_user, @commit, revert_into, revert_tree_id)
success
else
- error_msg = "Sorry, we cannot revert this #{params[:revert_type_title]} automatically.
+ error_msg = "Sorry, we cannot revert this #{@commit.change_type_title} automatically.
It may have already been reverted, or a more recent commit may have updated some of its content."
- raise ReversionError, error_msg
+ raise ChangeError, error_msg
end
end
-
- private
-
- def create_target_branch(new_branch)
- # Temporary branch exists and contains the revert commit
- return success if repository.find_branch(new_branch)
-
- result = CreateBranchService.new(@project, current_user)
- .execute(new_branch, @target_branch, source_project: @source_project)
-
- if result[:status] == :error
- raise ReversionError, "There was an error creating the source branch: #{result[:message]}"
- end
- end
-
- def check_push_permissions
- allowed = ::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(@target_branch)
-
- unless allowed
- raise ValidationError.new('You are not allowed to push into this branch')
- end
-
- true
- end
end
end
diff --git a/app/services/git_push_service.rb b/app/services/git_push_service.rb
index dc74c02760b..1e1be8cd04b 100644
--- a/app/services/git_push_service.rb
+++ b/app/services/git_push_service.rb
@@ -73,6 +73,7 @@ class GitPushService < BaseService
@project.update_merge_requests(params[:oldrev], params[:newrev], params[:ref], current_user)
EventCreateService.new.push(@project, current_user, build_push_data)
+ SystemHooksService.new.execute_hooks(build_push_data_system_hook.dup, :push_hooks)
@project.execute_hooks(build_push_data.dup, :push_hooks)
@project.execute_services(build_push_data.dup, :push_hooks)
CreateCommitBuildsService.new.execute(@project, current_user, build_push_data)
@@ -138,6 +139,11 @@ class GitPushService < BaseService
build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], push_commits)
end
+ def build_push_data_system_hook
+ @push_data_system ||= Gitlab::PushDataBuilder.
+ build(@project, current_user, params[:oldrev], params[:newrev], params[:ref], [])
+ end
+
def push_to_existing_branch?
# Return if this is not a push to a branch (e.g. new commits)
Gitlab::Git.branch_ref?(params[:ref]) && !Gitlab::Git.blank_ref?(params[:oldrev])
diff --git a/app/services/git_tag_push_service.rb b/app/services/git_tag_push_service.rb
index c88c7672805..64271d8bc5c 100644
--- a/app/services/git_tag_push_service.rb
+++ b/app/services/git_tag_push_service.rb
@@ -1,16 +1,16 @@
-class GitTagPushService
- attr_accessor :project, :user, :push_data
+class GitTagPushService < BaseService
+ attr_accessor :push_data
- def execute(project, user, oldrev, newrev, ref)
+ def execute
project.repository.before_push_tag
- @project, @user = project, user
- @push_data = build_push_data(oldrev, newrev, ref)
+ @push_data = build_push_data
- EventCreateService.new.push(project, user, @push_data)
+ EventCreateService.new.push(project, current_user, @push_data)
+ SystemHooksService.new.execute_hooks(build_system_push_data.dup, :tag_push_hooks)
project.execute_hooks(@push_data.dup, :tag_push_hooks)
project.execute_services(@push_data.dup, :tag_push_hooks)
- CreateCommitBuildsService.new.execute(project, @user, @push_data)
+ CreateCommitBuildsService.new.execute(project, current_user, @push_data)
ProjectCacheWorker.perform_async(project.id)
true
@@ -18,14 +18,14 @@ class GitTagPushService
private
- def build_push_data(oldrev, newrev, ref)
+ def build_push_data
commits = []
message = nil
- if !Gitlab::Git.blank_ref?(newrev)
- tag_name = Gitlab::Git.ref_name(ref)
+ if !Gitlab::Git.blank_ref?(params[:newrev])
+ tag_name = Gitlab::Git.ref_name(params[:ref])
tag = project.repository.find_tag(tag_name)
- if tag && tag.target == newrev
+ if tag && tag.target == params[:newrev]
commit = project.commit(tag.target)
commits = [commit].compact
message = tag.message
@@ -33,6 +33,11 @@ class GitTagPushService
end
Gitlab::PushDataBuilder.
- build(project, user, oldrev, newrev, ref, commits, message)
+ build(project, current_user, params[:oldrev], params[:newrev], params[:ref], commits, message)
+ end
+
+ def build_system_push_data
+ Gitlab::PushDataBuilder.
+ build(project, current_user, params[:oldrev], params[:newrev], params[:ref], [], '')
end
end
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index f0615ec7420..e43b5b51e5b 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -3,17 +3,13 @@ class SystemHooksService
execute_hooks(build_event_data(model, event))
end
- private
-
- def execute_hooks(data)
- SystemHook.all.each do |sh|
- async_execute_hook(sh, data, 'system_hooks')
+ def execute_hooks(data, hooks_scope = :all)
+ SystemHook.send(hooks_scope).each do |hook|
+ hook.async_execute(data, 'system_hooks')
end
end
- def async_execute_hook(hook, data, hook_name)
- Sidekiq::Client.enqueue(SystemHookWorker, hook.id, data, hook_name)
- end
+ private
def build_event_data(model, event)
data = {
diff --git a/app/services/wiki_pages/base_service.rb b/app/services/wiki_pages/base_service.rb
new file mode 100644
index 00000000000..9162f128602
--- /dev/null
+++ b/app/services/wiki_pages/base_service.rb
@@ -0,0 +1,27 @@
+module WikiPages
+ class BaseService < ::BaseService
+
+ def hook_data(page, action)
+ hook_data = {
+ object_kind: page.class.name.underscore,
+ user: current_user.hook_attrs,
+ project: @project.hook_attrs,
+ object_attributes: page.hook_attrs,
+ # DEPRECATED
+ repository: @project.hook_attrs.slice(:name, :url, :description, :homepage)
+ }
+
+ page_url = Gitlab::UrlBuilder.build(page)
+ hook_data[:object_attributes].merge!(url: page_url, action: action)
+ hook_data
+ end
+
+ private
+
+ def execute_hooks(page, action = 'create')
+ page_data = hook_data(page, action)
+ @project.execute_hooks(page_data, :wiki_page_hooks)
+ @project.execute_services(page_data, :wiki_page_hooks)
+ end
+ end
+end
diff --git a/app/services/wiki_pages/create_service.rb b/app/services/wiki_pages/create_service.rb
new file mode 100644
index 00000000000..988c663b9d0
--- /dev/null
+++ b/app/services/wiki_pages/create_service.rb
@@ -0,0 +1,13 @@
+module WikiPages
+ class CreateService < WikiPages::BaseService
+ def execute
+ page = WikiPage.new(@project.wiki)
+
+ if page.create(@params)
+ execute_hooks(page, 'create')
+ end
+
+ page
+ end
+ end
+end
diff --git a/app/services/wiki_pages/update_service.rb b/app/services/wiki_pages/update_service.rb
new file mode 100644
index 00000000000..8f6a50da838
--- /dev/null
+++ b/app/services/wiki_pages/update_service.rb
@@ -0,0 +1,11 @@
+module WikiPages
+ class UpdateService < WikiPages::BaseService
+ def execute(page)
+ if page.update(@params[:content], @params[:format], @params[:message])
+ execute_hooks(page, 'update')
+ end
+
+ page
+ end
+ end
+end
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index aadd2c54f20..e0d8d16a954 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -155,7 +155,11 @@
= f.label :shared_runners_enabled do
= f.check_box :shared_runners_enabled
Enable shared runners for new projects
-
+ .form-group
+ = f.label :shared_runners_text, class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.text_area :shared_runners_text, class: 'form-control', rows: 4
+ .help-block Markdown enabled
.form-group
= f.label :max_artifacts_size, 'Maximum artifacts size (MB)', class: 'control-label col-sm-2'
.col-sm-10
@@ -214,6 +218,13 @@
.help-block
The sampling interval in seconds. Sampled data includes memory usage,
retained Ruby objects, file descriptors and so on.
+ .form-group
+ = f.label :metrics_packet_size, 'Metrics per packet', class: 'control-label col-sm-2'
+ .col-sm-10
+ = f.number_field :metrics_packet_size, class: 'form-control'
+ .help-block
+ The amount of points to store in a single UDP packet. More points
+ results in fewer but larger UDP packets being sent.
%fieldset
%legend Spam and Anti-bot Protection
@@ -282,7 +293,7 @@
= f.check_box :repository_checks_enabled
Enable Repository Checks
.help-block
- GitLab will periodically run
+ GitLab will periodically run
%a{ href: 'https://www.kernel.org/pub/software/scm/git/docs/git-fsck.html', target: 'blank' } 'git fsck'
in all project and wiki repositories to look for silent disk corruption issues.
.form-group
@@ -290,7 +301,7 @@
= link_to 'Clear all repository checks', clear_repository_check_states_admin_application_settings_path, data: { confirm: 'This will clear repository check states for ALL projects in the database. This cannot be undone. Are you sure?' }, method: :put, class: "btn btn-sm btn-remove"
.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.
-
+
.form-actions
= f.submit 'Save', class: 'btn btn-save'
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index ad952052f25..67d23c80233 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -17,6 +17,27 @@
.col-sm-10
= f.text_field :url, class: "form-control"
.form-group
+ = f.label :url, "Trigger", class: 'control-label'
+ .col-sm-10.prepend-top-10
+ %div
+ System hook will be triggered on set of events like creating project
+ or adding ssh key. But you can also enable extra triggers like Push events.
+
+ %div.prepend-top-default
+ = f.check_box :push_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :push_events, class: 'list-label' do
+ %strong Push events
+ %p.light
+ This url will be triggered by a push to the repository
+ %div
+ = f.check_box :tag_push_events, class: 'pull-left'
+ .prepend-left-20
+ = f.label :tag_push_events, class: 'list-label' do
+ %strong Tag push events
+ %p.light
+ This url will be triggered when a new tag is pushed to the repository
+ .form-group
= f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
.col-sm-10
.checkbox
@@ -31,13 +52,16 @@
.panel.panel-default
.panel-heading
System hooks (#{@hooks.count})
- %ul.well-list
+ %ul.content-list
- @hooks.each do |hook|
%li
- .list-item-name
- %strong= hook.url
- %p SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
-
- .pull-right
+ .controls
= link_to 'Test Hook', admin_hook_test_path(hook), class: "btn btn-sm"
= link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
+ .monospace= hook.url
+ %div
+ - %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
+ - if hook.send(trigger)
+ %span.label.label-gray= trigger.titleize
+ %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
+
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index b05fdbd5552..fe0b9d3a491 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -7,17 +7,17 @@
.form-group
= f.label :name, class: 'control-label'
.col-sm-10
- = f.text_field :name, required: true, autocomplete: "off", class: 'form-control'
+ = f.text_field :name, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
.form-group
= f.label :username, class: 'control-label'
.col-sm-10
- = f.text_field :username, required: true, autocomplete: "off", class: 'form-control'
+ = f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control'
%span.help-inline * required
.form-group
= f.label :email, class: 'control-label'
.col-sm-10
- = f.text_field :email, required: true, autocomplete: "off", class: 'form-control'
+ = f.text_field :email, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
- if @user.new_record?
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index f9ec3a89158..49ab8aad1d5 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -45,6 +45,7 @@
.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} }
- @todos.group_by(&:project).each do |group|
.panel.panel-default.panel-small.js-todos-list
- project = group[0]
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 9639da4cb58..5b7f11440c1 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -26,7 +26,7 @@
- @already_added_projects.each do |project|
%tr{id: "project_#{project.id}", class: "#{project_status_css_class(project.import_status)}"}
%td
- = link_to project.import_source, "https://github.com/#{project.import_source}", target: "_blank"
+ = github_project_link(project.import_source)
%td
= link_to project.path_with_namespace, [project.namespace.becomes(Namespace), project]
%td.job-status
@@ -43,7 +43,7 @@
- @repos.each do |repo|
%tr{id: "repo_#{repo.id}"}
%td
- = link_to repo.full_name, "https://github.com/#{repo.full_name}", target: "_blank"
+ = github_project_link(repo.full_name)
%td.import-target
= repo.full_name
%td.import-actions.job-status
diff --git a/app/views/notify/closed_merge_request_email.html.haml b/app/views/notify/closed_merge_request_email.html.haml
index 574e8bfef24..81c7c88fc96 100644
--- a/app/views/notify/closed_merge_request_email.html.haml
+++ b/app/views/notify/closed_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}"
+ = "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
diff --git a/app/views/notify/closed_merge_request_email.text.haml b/app/views/notify/closed_merge_request_email.text.haml
index 59db86b08bc..b435067d5a6 100644
--- a/app/views/notify/closed_merge_request_email.text.haml
+++ b/app/views/notify/closed_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request ##{@merge_request.iid} was closed by #{@updated_by.name}"
+= "Merge Request #{@merge_request.to_reference} was closed by #{@updated_by.name}"
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/merge_request_status_email.html.haml b/app/views/notify/merge_request_status_email.html.haml
index c9bf04f514e..41a320d6bd8 100644
--- a/app/views/notify/merge_request_status_email.html.haml
+++ b/app/views/notify/merge_request_status_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}"
+ = "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
diff --git a/app/views/notify/merge_request_status_email.text.haml b/app/views/notify/merge_request_status_email.text.haml
index b96dd0fd8ab..7a5074a1dc3 100644
--- a/app/views/notify/merge_request_status_email.text.haml
+++ b/app/views/notify/merge_request_status_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request ##{@merge_request.iid} was #{@mr_status} by #{@updated_by.name}"
+= "Merge Request #{@merge_request.to_reference} was #{@mr_status} by #{@updated_by.name}"
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/merged_merge_request_email.html.haml b/app/views/notify/merged_merge_request_email.html.haml
index 6762fae7f64..fbe506d4f4d 100644
--- a/app/views/notify/merged_merge_request_email.html.haml
+++ b/app/views/notify/merged_merge_request_email.html.haml
@@ -1,2 +1,2 @@
%p
- = "Merge Request ##{@merge_request.iid} was merged"
+ = "Merge Request #{@merge_request.to_reference} was merged"
diff --git a/app/views/notify/merged_merge_request_email.text.haml b/app/views/notify/merged_merge_request_email.text.haml
index 34dbc60e19b..bfbae01094f 100644
--- a/app/views/notify/merged_merge_request_email.text.haml
+++ b/app/views/notify/merged_merge_request_email.text.haml
@@ -1,4 +1,4 @@
-= "Merge Request ##{@merge_request.iid} was merged"
+= "Merge Request #{@merge_request.to_reference} was merged"
Merge Request url: #{namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)}
diff --git a/app/views/notify/new_merge_request_email.text.erb b/app/views/notify/new_merge_request_email.text.erb
index bdcca6e4ab7..d4aad8d1862 100644
--- a/app/views/notify/new_merge_request_email.text.erb
+++ b/app/views/notify/new_merge_request_email.text.erb
@@ -1,4 +1,4 @@
-New Merge Request #<%= @merge_request.iid %>
+New Merge Request <%= @merge_request.to_reference %>
<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request)) %>
diff --git a/app/views/notify/note_merge_request_email.text.erb b/app/views/notify/note_merge_request_email.text.erb
index 1d1411992a6..8cdab63829e 100644
--- a/app/views/notify/note_merge_request_email.text.erb
+++ b/app/views/notify/note_merge_request_email.text.erb
@@ -1,4 +1,4 @@
-New comment for Merge Request <%= @merge_request.iid %>
+New comment for Merge Request <%= @merge_request.to_reference %>
<%= url_for(namespace_project_merge_request_url(@merge_request.target_project.namespace, @merge_request.target_project, @merge_request, anchor: "note_#{@note.id}")) %>
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index 961b61d2e76..48b0dd6b121 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -9,4 +9,7 @@
= spinner
:javascript
- new Activities();
+ var activity = new Activities();
+ $(document).on('page:restore', function (event) {
+ activity.reloadActivities()
+ })
diff --git a/app/views/projects/_readme.html.haml b/app/views/projects/_readme.html.haml
index d1191928d4f..a9908eaecca 100644
--- a/app/views/projects/_readme.html.haml
+++ b/app/views/projects/_readme.html.haml
@@ -9,7 +9,7 @@
- else
.gray-content-block.second-block.center
%h3.page-title
- This project does not have README yet
+ This project does not have a README yet
- if can?(current_user, :push_code, @project)
%p
A
@@ -18,5 +18,5 @@
distributed with computer software, forming part of its documentation.
%p
We recommend you to
- = link_to "add README", new_readme_path, class: 'underlined-link'
+ = link_to "add a README", new_readme_path, class: 'underlined-link'
file to the repository and GitLab will render it here instead of this message.
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index f8b6fa253c4..fefa652a3da 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -13,7 +13,11 @@
required: true, class: 'form-control new-file-name'
.pull-right
- = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
+ .license-selector.js-license-selector.hide
+ = select_tag :license_type, grouped_options_for_select(licenses_for_select, @project.repository.license_key), include_blank: true, class: 'select2 license-select', data: {placeholder: 'Choose a license template', project: @project.name, fullname: @project.namespace.human_name}
+
+ .encoding-selector
+ = select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
.file-content.code
%pre.js-edit-mode-pane#editor #{params[:content] || local_assigns[:blob_data]}
diff --git a/app/views/projects/blob/new.html.haml b/app/views/projects/blob/new.html.haml
index 1dd2b5c0af7..0459699432e 100644
--- a/app/views/projects/blob/new.html.haml
+++ b/app/views/projects/blob/new.html.haml
@@ -14,5 +14,5 @@
cancel_path: namespace_project_tree_path(@project.namespace, @project, @id)
:javascript
- blob = new NewBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}", null)
+ blob = new EditBlob(gon.relative_url_root + "#{Gitlab::Application.config.assets.prefix}")
new NewCommitForm($('.js-new-blob-form'))
diff --git a/app/views/projects/builds/show.html.haml b/app/views/projects/builds/show.html.haml
index b02aee3db21..6d4505ebb60 100644
--- a/app/views/projects/builds/show.html.haml
+++ b/app/views/projects/builds/show.html.haml
@@ -10,7 +10,7 @@
- merge_request = @build.merge_request
- if merge_request
via
- = link_to "merge request ##{merge_request.iid}", merge_request_path(merge_request)
+ = link_to "merge request #{merge_request.to_reference}", merge_request_path(merge_request)
#up-build-trace
- builds = @build.commit.matrix_builds(@build)
@@ -110,7 +110,7 @@
= icon('folder-open')
Browse
- .build-widget
+ .build-widget.build-controls
%h4.title
Build ##{@build.id}
- if can?(current_user, :update_build, @project)
@@ -127,6 +127,9 @@
data: { confirm: 'Are you sure you want to erase this build?' } do
= icon('eraser')
Erase
+ - if @build.has_trace?
+ = link_to 'Raw', raw_namespace_project_build_path(@project.namespace, @project, @build),
+ class: 'btn btn-sm btn-success', target: '_blank'
.clearfix
- if @build.duration
diff --git a/app/views/projects/commit/_revert.html.haml b/app/views/projects/commit/_change.html.haml
index 52ca3ed5b14..44ef1fdbbe3 100644
--- a/app/views/projects/commit/_revert.html.haml
+++ b/app/views/projects/commit/_change.html.haml
@@ -1,13 +1,21 @@
-#modal-revert-commit.modal
+- case type.to_s
+- when 'revert'
+ - label = 'Revert'
+ - target_label = 'Revert in branch'
+- when 'cherry-pick'
+ - label = 'Cherry-pick'
+ - target_label = 'Pick into branch'
+
+.modal{id: "modal-#{type}-commit"}
.modal-dialog
.modal-content
.modal-header
%a.close{href: "#", "data-dismiss" => "modal"} Ă—
- %h3.page-title== Revert this #{revert_commit_type(commit)}
+ %h3.page-title== #{label} this #{commit.change_type_title}
.modal-body
- = form_tag revert_namespace_project_commit_path(@project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-create-dir-form js-requires-input' do
+ = form_tag send("#{type.underscore}_namespace_project_commit_path", @project.namespace, @project, commit.id), method: :post, remote: false, class: 'form-horizontal js-#{type}-form js-requires-input' do
.form-group.branch
- = label_tag 'target_branch', 'Revert in branch', class: 'control-label'
+ = label_tag 'target_branch', target_label, class: 'control-label'
.col-sm-10
= select_tag "target_branch", grouped_options_refs, class: "select2 select2-sm js-target-branch"
- if can?(current_user, :push_code, @project)
@@ -20,7 +28,7 @@
- else
= hidden_field_tag 'create_merge_request', 1
.form-actions
- = submit_tag "Revert", class: 'btn btn-create'
+ = submit_tag label, class: 'btn btn-create'
= link_to "Cancel", '#', class: "btn btn-cancel", "data-dismiss" => "modal"
- unless can?(current_user, :push_code, @project)
@@ -28,4 +36,4 @@
= commit_in_fork_help
:javascript
- new NewCommitForm($('.js-create-dir-form'))
+ new NewCommitForm($('.js-#{type}-form'))
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 71995fcc487..d6c9e54e657 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -18,6 +18,7 @@
Browse Files
- unless @commit.has_been_reverted?(current_user)
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id))
+ = cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id))
%div
%p
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 21e186120c3..e550af7888a 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -13,4 +13,5 @@
diff_refs: @diff_refs
= render "projects/notes/notes_with_form"
- if can_collaborate_with_project?
- = render "projects/commit/revert", commit: @commit, title: @commit.title
+ - %w(revert cherry-pick).each do |type|
+ = render "projects/commit/change", type: type, commit: @commit, title: @commit.title
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 34f27f1e793..d71f61466f1 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -31,5 +31,5 @@
by
= commit_author_link(commit, avatar: true, size: 24)
.committed_ago
- #{time_ago_with_tooltip(commit.committed_date, skip_js: true)} &nbsp;
+ #{time_ago_with_tooltip(commit.committed_date)} &nbsp;
= link_to_browse_code(project, commit)
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 6ad7b05155a..52d093871b4 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -14,8 +14,10 @@
%p
If you already have files you can push them using command line instructions below.
%p
- Otherwise you can start with
- = link_to "adding README", new_readme_path, class: 'underlined-link'
+ Otherwise you can start with adding a
+ = link_to "README", new_readme_path, class: 'underlined-link'
+ or a
+ = link_to "LICENSE", add_special_file_path(@project, file_name: 'LICENSE'), class: 'underlined-link'
file to this project.
- if can?(current_user, :push_code, @project)
diff --git a/app/views/projects/hooks/index.html.haml b/app/views/projects/hooks/index.html.haml
index e39224d86c6..aae3abcad4b 100644
--- a/app/views/projects/hooks/index.html.haml
+++ b/app/views/projects/hooks/index.html.haml
@@ -74,16 +74,15 @@
.panel.panel-default
.panel-heading
Webhooks (#{@hooks.count})
- %ul.well-list
+ %ul.content-list
- @hooks.each do |hook|
%li
- .pull-right
+ .controls
= link_to 'Test Hook', test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm btn-grouped"
= link_to 'Remove', namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-remove btn-sm btn-grouped"
- .clearfix
- %span.monospace= hook.url
- %p
+ .monospace= hook.url
+ %div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
- SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
+ %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
diff --git a/app/views/projects/issues/_issue.html.haml b/app/views/projects/issues/_issue.html.haml
index 7a8009f6da4..9ad86ed71c9 100644
--- a/app/views/projects/issues/_issue.html.haml
+++ b/app/views/projects/issues/_issue.html.haml
@@ -48,6 +48,11 @@
= link_to namespace_project_issues_path(issue.project.namespace, issue.project, milestone_title: issue.milestone.title) do
= icon('clock-o')
= issue.milestone.title
+ - if issue.due_date
+ %span{class: "#{'cred' if issue.overdue?}"}
+ &nbsp;
+ = icon('calendar')
+ = issue.due_date.to_s(:medium)
- if issue.labels.any?
&nbsp;
- issue.labels.each do |label|
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index 7125d7d9d1c..8d05060f563 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -86,8 +86,10 @@
= spinner
= render 'shared/issuable/sidebar', issuable: @merge_request
-- if @merge_request.can_be_reverted?
- = render "projects/commit/revert", commit: @merge_request.merge_commit, title: @merge_request.title
+- if @merge_request.can_be_reverted?(current_user)
+ = render "projects/commit/change", type: 'revert', commit: @merge_request.merge_commit, title: @merge_request.title
+- if @merge_request.can_be_cherry_picked?
+ = render "projects/commit/change", type: 'cherry-pick', commit: @merge_request.merge_commit, title: @merge_request.title
:javascript
var merge_request;
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index fc62bb5bce9..b31ea5e5321 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,7 +1,7 @@
-- page_title "Edit", "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
= render "header_title"
%h3.page-title
- Edit Merge Request ##{@merge_request.iid}
+ Edit Merge Request #{@merge_request.to_reference}
%hr
= render 'form'
diff --git a/app/views/projects/merge_requests/invalid.html.haml b/app/views/projects/merge_requests/invalid.html.haml
index fc03ee73a3d..8ac653427c9 100644
--- a/app/views/projects/merge_requests/invalid.html.haml
+++ b/app/views/projects/merge_requests/invalid.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@merge_request.title} (##{@merge_request.iid})", "Merge Requests"
+- page_title "#{@merge_request.title} (#{merge_request.to_reference}", "Merge Requests"
= render "header_title"
.merge-request
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 0a99e8c9591..36c275e8be1 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -24,9 +24,9 @@
%li{ class: issue_button_visibility(@merge_request, false) }
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: 'reopen-mr-link', title: 'Reopen merge request'
%li
- = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit', id: 'edit_merge_request'
+ = link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: 'issuable-edit'
= link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-close #{issue_button_visibility(@merge_request, true)}", title: 'Close merge request'
= link_to 'Reopen', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "hidden-xs hidden-sm btn btn-nr btn-grouped btn-reopen reopen-mr-link #{issue_button_visibility(@merge_request, false)}", title: 'Reopen merge request'
- = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit", id: 'edit_merge_request' do
+ = link_to edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "hidden-xs hidden-sm btn btn-nr btn-grouped issuable-edit" do
= icon('pencil-square-o')
Edit
diff --git a/app/views/projects/merge_requests/widget/_merged.html.haml b/app/views/projects/merge_requests/widget/_merged.html.haml
index 3abae9f0bf6..ec4beae9727 100644
--- a/app/views/projects/merge_requests/widget/_merged.html.haml
+++ b/app/views/projects/merge_requests/widget/_merged.html.haml
@@ -44,3 +44,8 @@
$('.remove_source_branch_in_progress').hide();
$('.remove_source_branch_widget.failed').show();
});
+ - else
+ %p
+ The changes were merged into
+ #{link_to @merge_request.target_branch, namespace_project_commits_path(@project.namespace, @project, @merge_request.target_branch), class: "label-branch"}.
+ = render 'projects/merge_requests/widget/merged_buttons'
diff --git a/app/views/projects/merge_requests/widget/_merged_buttons.haml b/app/views/projects/merge_requests/widget/_merged_buttons.haml
index 85a3a6ba9e2..56167509af9 100644
--- a/app/views/projects/merge_requests/widget/_merged_buttons.haml
+++ b/app/views/projects/merge_requests/widget/_merged_buttons.haml
@@ -1,11 +1,14 @@
-- source_branch_exists = local_assigns.fetch(:source_branch_exists, false)
-- mr_can_be_reverted = @merge_request.can_be_reverted?
+- can_remove_source_branch = local_assigns.fetch(:source_branch_exists, false) && @merge_request.can_remove_source_branch?(current_user)
+- mr_can_be_reverted = @merge_request.can_be_reverted?(current_user)
+- mr_can_be_cherry_picked = @merge_request.can_be_cherry_picked?
-- if source_branch_exists || mr_can_be_reverted
+- if can_remove_source_branch || mr_can_be_reverted || mr_can_be_cherry_picked
.btn-group
- - if source_branch_exists
+ - if can_remove_source_branch
= link_to namespace_project_branch_path(@merge_request.source_project.namespace, @merge_request.source_project, @merge_request.source_branch), remote: true, method: :delete, class: "btn btn-default btn-grouped btn-sm remove_source_branch" do
= icon('trash-o')
Remove Source Branch
- if mr_can_be_reverted
= revert_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
+ - if mr_can_be_cherry_picked
+ = cherry_pick_commit_link(@merge_request.merge_commit, namespace_project_merge_request_path(@project.namespace, @project, @merge_request), btn_class: 'sm')
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 6e9ecdf7649..aeb7c1d5ee4 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -8,7 +8,9 @@
.note-header
= link_to_member(note.project, note.author, avatar: false)
.inline.note-headline-light
- = "#{note.author.to_reference} commented"
+ = "#{note.author.to_reference}"
+ - if !note.system
+ commented
%a{ href: "##{dom_id(note)}" }
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
.note-actions
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 6a37f444bb7..9fa4127c948 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,7 +1,10 @@
%h3 Shared runners
-.bs-callout.bs-callout-warning
- GitLab Runners do not offer secure isolation between projects that they do builds for. You are TRUSTING all GitLab users who can push code to project A, B or C to run shell scripts on the machine hosting runner X.
+.bs-callout.bs-callout-warning.shared-runners-description
+ - if shared_runners_text.present?
+ = markdown(shared_runners_text, pipeline: 'plain_markdown')
+ - else
+ Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com).
%hr
- if @project.shared_runners_enabled?
= link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 4310f038fc9..d854ac21725 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -36,9 +36,9 @@
%li
= link_to 'Changelog', changelog_path(@project)
- - if @repository.license
+ - if @repository.license_blob
%li
- = link_to 'License', license_path(@project)
+ = link_to license_short_name(@project), license_path(@project)
- if @repository.contribution_guide
%li
@@ -47,15 +47,15 @@
- if current_user && can_push_branch?(@project, @project.default_branch)
- unless @repository.changelog
%li.missing
- = link_to add_changelog_path(@project) do
+ = link_to add_special_file_path(@project, file_name: 'CHANGELOG') do
Add Changelog
- - unless @repository.license
+ - unless @repository.license_blob
%li.missing
- = link_to add_license_path(@project) do
+ = link_to add_special_file_path(@project, file_name: 'LICENSE') do
Add License
- unless @repository.contribution_guide
%li.missing
- = link_to add_contribution_guide_path(@project) do
+ = link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide
- if @repository.commit
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index faeb2b55c6f..333f6533213 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -2,7 +2,7 @@
%h4
= link_to [merge_request.target_project.namespace.becomes(Namespace), merge_request.target_project, merge_request] do
%span.term.str-truncated= merge_request.title
- .pull-right ##{merge_request.iid}
+ .pull-right #{merge_request.to_reference}
- if merge_request.description.present?
.description.term
= preserve do
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index b38c5e18efb..9ce5562e667 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -2,4 +2,4 @@
%span.label-name
= link_to_label(label, tooltip: false)
%span.prepend-left-10
- = markdown(label.description, pipeline: :single_line)
+ = markdown(label.description, pipeline: :single_line) \ No newline at end of file
diff --git a/app/views/shared/_labels_row.html.haml b/app/views/shared/_labels_row.html.haml
new file mode 100644
index 00000000000..dc89e36419c
--- /dev/null
+++ b/app/views/shared/_labels_row.html.haml
@@ -0,0 +1,3 @@
+- labels.each do |label|
+ %span.label-row
+ = link_to_label(label, tooltip: false)
diff --git a/app/views/shared/_service_settings.html.haml b/app/views/shared/_service_settings.html.haml
index fc935166bf6..4eaf7c2a025 100644
--- a/app/views/shared/_service_settings.html.haml
+++ b/app/views/shared/_service_settings.html.haml
@@ -62,6 +62,14 @@
%strong Build events
%p.light
This url will be triggered when a build status changes
+ - if @service.supported_events.include?("wiki_page")
+ %div
+ = form.check_box :wiki_page_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :wiki_page_events, class: 'list-label' do
+ %strong Wiki Page events
+ %p.light
+ This url will be triggered when a wiki page is created/updated
- @service.fields.each do |field|
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index e3a6a5a68b6..d327bd0a96f 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -20,6 +20,11 @@
= sort_title_milestone_soon
= link_to page_filter_path(sort: sort_value_milestone_later) do
= sort_title_milestone_later
+ - if controller.controller_name == 'issues' || controller.action_name == 'issues'
+ = link_to page_filter_path(sort: sort_value_due_date_soon) do
+ = sort_title_due_date_soon
+ = link_to page_filter_path(sort: sort_value_due_date_later) do
+ = sort_title_due_date_later
= link_to page_filter_path(sort: sort_value_upvotes) do
= sort_title_upvotes
= link_to page_filter_path(sort: sort_value_downvotes) do
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index 921eaefd79a..fc1101646fb 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -23,6 +23,7 @@
.filter-item.inline.labels-filter
= render "shared/issuable/label_dropdown"
+
.pull-right
= render 'shared/sort_dropdown'
@@ -46,9 +47,10 @@
.filter-item.inline
= button_tag "Update issues", class: "btn update_selected_issues btn-save"
-- if @label
- .gray-content-block.second-block
- = render "shared/label_row", label: @label
+ - if !@labels.nil?
+ .gray-content-block.second-block.filtered-labels{ class: ("hidden" if !@labels.any?) }
+ - if @labels.any?
+ = render "shared/labels_row", labels: @labels
:javascript
new UsersSelect();
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index bae15b7f844..18b091df39b 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -75,8 +75,9 @@
= f.label :label_ids, "Labels", class: 'control-label'
.col-sm-10{ class: ('issuable-form-padding-top' if !has_labels) }
- if has_labels
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
+ .issuable-form-select-holder
+ = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
+ { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- else
%span.light No labels yet.
&nbsp;
@@ -88,9 +89,10 @@
.form-group
= label_tag :move_to_project_id, 'Move', class: 'control-label'
.col-sm-10
- - projects = project_options(issuable, current_user, ability: :admin_issue)
- = select_tag(:move_to_project_id, projects, include_blank: true,
- class: 'select2', data: { placeholder: 'Select project' })
+ .issuable-form-select-holder
+ - projects = project_options(issuable, current_user, ability: :admin_issue)
+ = select_tag(:move_to_project_id, projects, include_blank: true,
+ class: 'select2', data: { placeholder: 'Select project' })
&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.' }
@@ -102,13 +104,15 @@
.form-group
= f.label :source_branch, class: 'control-label'
.col-sm-10
- = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
+ .issuable-form-select-holder
+ = f.select(:source_branch, [@merge_request.source_branch], { }, { class: 'source_branch select2 span2', disabled: true })
.form-group
= f.label :target_branch, class: 'control-label'
.col-sm-10
- = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
+ .issuable-form-select-holder
+ = f.select(:target_branch, @merge_request.target_branches, { include_blank: true }, { class: 'target_branch select2 span2', disabled: @merge_request.new_record?, data: {placeholder: "Select branch"} })
- if @merge_request.new_record?
- %p.help-block
+ &nbsp;
= link_to 'Change branches', mr_change_branches_path(@merge_request)
- is_footer = !(issuable.is_a?(MergeRequest) && issuable.new_record?)
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index f722e61eeac..3eaa45258f0 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -1,9 +1,11 @@
- if params[:label_name].present?
- = hidden_field_tag(:label_name, params[:label_name])
+ - if params[:label_name].respond_to?('any?')
+ - params[:label_name].each do |label|
+ = hidden_field_tag "label_name[]", label, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
+ %button.dropdown-menu-toggle.js-label-select.js-filter-submit.js-multiselect.js-extra-options{type: "button", data: {toggle: "dropdown", field_name: "label_name[]", show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}}
%span.dropdown-toggle-text
- = h(params[:label_name].presence || "Label")
+ = h(multi_label_name(params[:label_name], "Label"))
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
.dropdown-page-one
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index a6970b7eebb..1d9b09a5ef1 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -4,22 +4,22 @@
- else
- page_context_word = 'issues'
%li{class: ("active" if params[:state] == 'opened')}
- = link_to page_filter_path(state: 'opened'), title: "Filter by #{page_context_word} that are currently opened." do
+ = link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
#{state_filters_text_for(:opened, @project)}
- if defined?(type) && type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')}
- = link_to page_filter_path(state: 'merged'), title: 'Filter by merge requests that are currently merged.' do
+ = link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
#{state_filters_text_for(:merged, @project)}
%li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed'), title: 'Filter by merge requests that are currently closed and unmerged.' do
+ = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
#{state_filters_text_for(:closed, @project)}
- else
%li{class: ("active" if params[:state] == 'closed')}
- = link_to page_filter_path(state: 'closed'), title: 'Filter by issues that are currently closed.' do
+ = link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
#{state_filters_text_for(:closed, @project)}
%li{class: ("active" if params[:state] == 'all')}
- = link_to page_filter_path(state: 'all'), title: "Show all #{page_context_word}." do
+ = link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
#{state_filters_text_for(:all, @project)}
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 08bfd93f4e6..a50b4b96693 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -10,14 +10,14 @@
= sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group{role: 'group', "aria-label" => '...'}
- if prev_issuable = prev_issuable_for(issuable)
- = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn'
+ = link_to 'Prev', [@project.namespace.becomes(Namespace), @project, prev_issuable], class: 'btn btn-default prev-btn issuable-pager'
- else
- %a.btn.btn-default.disabled{href: '#'}
+ %a.btn.btn-default.issuable-pager.disabled{href: '#'}
Prev
- if next_issuable = next_issuable_for(issuable)
- = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn'
+ = link_to 'Next', [@project.namespace.becomes(Namespace), @project, next_issuable], class: 'btn btn-default next-btn issuable-pager'
- else
- %a.btn.btn-default.disabled{href: '#'}
+ %a.btn.btn-default.issuable-pager.disabled{href: '#'}
Next
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
@@ -58,7 +58,7 @@
- if issuable.milestone
= issuable.milestone.title
- else
- No
+ None
.title.hide-collapsed
Milestone
= icon('spinner spin', class: 'block-loading')
@@ -75,6 +75,34 @@
= f.hidden_field 'milestone_id', value: issuable.milestone_id, id: nil
= dropdown_tag('Milestone', options: { title: 'Assign milestone', toggle_class: 'js-milestone-select js-extra-options', filter: true, dropdown_class: 'dropdown-menu-selectable', placeholder: 'Search milestones', data: { show_no: true, field_name: "#{issuable.to_ability_name}[milestone_id]", project_id: @project.id, issuable_id: issuable.id, milestones: namespace_project_milestones_path(@project.namespace, @project, :json), ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable), use_id: true }})
+ - if issuable.has_attribute?(:due_date)
+ .block.due_date
+ .sidebar-collapsed-icon
+ = icon('calendar')
+ %span.js-due-date-sidebar-value
+ = issuable.due_date.try(:to_s, :medium) || 'None'
+ .title.hide-collapsed
+ Due date
+ = icon('spinner spin', class: 'block-loading')
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ = link_to 'Edit', '#', class: 'edit-link pull-right'
+ .value.bold.hide-collapsed
+ - if issuable.due_date
+ = issuable.due_date.to_s(:medium)
+ - else
+ .light None
+ - if can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
+ .selectbox.hide-collapsed
+ = f.hidden_field :due_date, value: issuable.due_date
+ .dropdown
+ %button.dropdown-menu-toggle.js-due-date-select{ type: 'button', data: { toggle: 'dropdown', field_name: "#{issuable.to_ability_name}[due_date]", ability_name: issuable.to_ability_name, issue_update: issuable_json_path(issuable) } }
+ %span.dropdown-toggle-text Due date
+ = icon('chevron-down')
+ .dropdown-menu.dropdown-menu-due-date
+ = dropdown_title('Due date')
+ = dropdown_content do
+ .js-due-date-calendar
+
- if issuable.project.labels.any?
.block.labels
.sidebar-collapsed-icon
@@ -118,6 +146,7 @@
Manage labels
- else
View labels
+ = dropdown_loading
= render "shared/issuable/participants", participants: issuable.participants(current_user)
- if current_user
@@ -150,6 +179,7 @@
:javascript
new MilestoneSelect('{"namespace":"#{@project.namespace.path}","path":"#{@project.path}"}');
new LabelsSelect();
- new IssuableContext('#{current_user.to_json(only: [:username, :id, :name])}');
+ new IssuableContext('#{escape_javascript(current_user.to_json(only: [:username, :id, :name]))}');
new Subscription('.subscription')
new Sidebar();
+ new DueDateSelect();
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index e1127b2311c..47b66d44e43 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -23,5 +23,5 @@
- if assignee
= link_to polymorphic_path(base_url_args, { milestone_title: @milestone.title, assignee_id: issuable.assignee_id, state: 'all' }),
- class: 'has-tooltip', data: { 'original-title' => "Assigned to #{sanitize(assignee.name)}", container: 'body' } do
+ class: 'has-tooltip', title: "Assigned to #{assignee.name}", data: { container: 'body' } do
- image_tag(avatar_icon(issuable.assignee, 16), class: "avatar s16", alt: '')
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 0c4b6a5618b..3028491e5b6 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -6,8 +6,6 @@
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, user_url(@user, format: :atom), title: "#{@user.name} activity")
-= render 'shared/show_aside'
-
.user-profile
.cover-block
.cover-controls
diff --git a/app/workers/post_receive.rb b/app/workers/post_receive.rb
index 9e1215b21a6..f3327ca9e61 100644
--- a/app/workers/post_receive.rb
+++ b/app/workers/post_receive.rb
@@ -39,7 +39,7 @@ class PostReceive
end
if Gitlab::Git.tag_ref?(ref)
- GitTagPushService.new.execute(post_received.project, @user, oldrev, newrev, ref)
+ GitTagPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
elsif Gitlab::Git.branch_ref?(ref)
GitPushService.new(post_received.project, @user, oldrev: oldrev, newrev: newrev, ref: ref).execute
end
@@ -47,7 +47,7 @@ class PostReceive
end
private
-
+
def log(message)
Gitlab::GitLogger.error("POST-RECEIVE: #{message}")
end
diff --git a/config/routes.rb b/config/routes.rb
index 46a25262844..79b62a0b1bb 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -549,6 +549,7 @@ Rails.application.routes.draw do
post :cancel_builds
post :retry_builds
post :revert
+ post :cherry_pick
end
end
@@ -668,6 +669,7 @@ Rails.application.routes.draw do
post :cancel
post :retry
post :erase
+ get :raw
end
resource :artifacts, only: [] do
diff --git a/db/migrate/20160227120001_add_event_field_for_web_hook.rb b/db/migrate/20160227120001_add_event_field_for_web_hook.rb
new file mode 100644
index 00000000000..65f2a47bb3c
--- /dev/null
+++ b/db/migrate/20160227120001_add_event_field_for_web_hook.rb
@@ -0,0 +1,5 @@
+class AddEventFieldForWebHook < ActiveRecord::Migration
+ def change
+ add_column :web_hooks, :wiki_page_events, :boolean, default: false, null: false
+ end
+end
diff --git a/db/migrate/20160227120047_add_event_to_services.rb b/db/migrate/20160227120047_add_event_to_services.rb
new file mode 100644
index 00000000000..f5040d770de
--- /dev/null
+++ b/db/migrate/20160227120047_add_event_to_services.rb
@@ -0,0 +1,5 @@
+class AddEventToServices < ActiveRecord::Migration
+ def change
+ add_column :services, :wiki_page_events, :boolean, default: true
+ end
+end
diff --git a/db/migrate/20160310124959_add_due_date_to_issues.rb b/db/migrate/20160310124959_add_due_date_to_issues.rb
new file mode 100644
index 00000000000..ec08bd9fdfa
--- /dev/null
+++ b/db/migrate/20160310124959_add_due_date_to_issues.rb
@@ -0,0 +1,6 @@
+class AddDueDateToIssues < ActiveRecord::Migration
+ def change
+ add_column :issues, :due_date, :date
+ add_index :issues, :due_date
+ end
+end
diff --git a/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb
new file mode 100644
index 00000000000..d493044c67b
--- /dev/null
+++ b/db/migrate/20160415133440_add_shared_runners_text_to_application_settings.rb
@@ -0,0 +1,5 @@
+class AddSharedRunnersTextToApplicationSettings < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :shared_runners_text, :text
+ end
+end
diff --git a/db/migrate/20160419120017_add_metrics_packet_size.rb b/db/migrate/20160419120017_add_metrics_packet_size.rb
new file mode 100644
index 00000000000..78c163d62ac
--- /dev/null
+++ b/db/migrate/20160419120017_add_metrics_packet_size.rb
@@ -0,0 +1,5 @@
+class AddMetricsPacketSize < ActiveRecord::Migration
+ def change
+ add_column :application_settings, :metrics_packet_size, :integer, default: 1
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 42c261003bb..a93ba690730 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: 20160412140240) do
+ActiveRecord::Schema.define(version: 20160419120017) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -78,6 +78,8 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.boolean "email_author_in_body", default: false
t.integer "default_group_visibility"
t.boolean "repository_checks_enabled", default: true
+ t.integer "metrics_packet_size", default: 1
+ t.text "shared_runners_text"
end
create_table "audit_events", force: :cascade do |t|
@@ -420,6 +422,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.integer "moved_to_id"
t.boolean "confidential", default: false
t.datetime "deleted_at"
+ t.date "due_date"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -429,6 +432,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do
add_index "issues", ["created_at"], name: "index_issues_on_created_at", using: :btree
add_index "issues", ["deleted_at"], name: "index_issues_on_deleted_at", using: :btree
add_index "issues", ["description"], name: "index_issues_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ add_index "issues", ["due_date"], name: "index_issues_on_due_date", using: :btree
add_index "issues", ["milestone_id"], name: "index_issues_on_milestone_id", using: :btree
add_index "issues", ["project_id", "iid"], name: "index_issues_on_project_id_and_iid", unique: true, using: :btree
add_index "issues", ["project_id"], name: "index_issues_on_project_id", using: :btree
@@ -716,37 +720,37 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.boolean "issues_enabled", default: true, null: false
- t.boolean "wall_enabled", default: true, null: false
- t.boolean "merge_requests_enabled", default: true, null: false
- t.boolean "wiki_enabled", default: true, null: false
+ t.boolean "issues_enabled", default: true, null: false
+ t.boolean "wall_enabled", default: true, null: false
+ t.boolean "merge_requests_enabled", default: true, null: false
+ t.boolean "wiki_enabled", default: true, null: false
t.integer "namespace_id"
- t.string "issues_tracker", default: "gitlab", null: false
+ t.string "issues_tracker", default: "gitlab", null: false
t.string "issues_tracker_id"
- t.boolean "snippets_enabled", default: true, null: false
+ t.boolean "snippets_enabled", default: true, null: false
t.datetime "last_activity_at"
t.string "import_url"
- t.integer "visibility_level", default: 0, null: false
- t.boolean "archived", default: false, null: false
+ t.integer "visibility_level", default: 0, null: false
+ t.boolean "archived", default: false, null: false
t.string "avatar"
t.string "import_status"
- t.float "repository_size", default: 0.0
- t.integer "star_count", default: 0, null: false
+ t.float "repository_size", default: 0.0
+ t.integer "star_count", default: 0, null: false
t.string "import_type"
t.string "import_source"
- t.integer "commit_count", default: 0
+ t.integer "commit_count", default: 0
t.text "import_error"
t.integer "ci_id"
- t.boolean "builds_enabled", default: true, null: false
- t.boolean "shared_runners_enabled", default: true, null: false
+ t.boolean "builds_enabled", default: true, null: false
+ t.boolean "shared_runners_enabled", default: true, null: false
t.string "runners_token"
t.string "build_coverage_regex"
- t.boolean "build_allow_git_fetch", default: true, null: false
- t.integer "build_timeout", default: 3600, null: false
- t.boolean "pending_delete", default: false
- t.boolean "public_builds", default: true, null: false
+ t.boolean "build_allow_git_fetch", default: true, null: false
+ t.integer "build_timeout", default: 3600, null: false
+ t.boolean "pending_delete", default: false
+ t.boolean "public_builds", default: true, null: false
t.string "main_language"
- t.integer "pushes_since_gc", default: 0
+ t.integer "pushes_since_gc", default: 0
t.boolean "last_repository_check_failed"
t.datetime "last_repository_check_at"
end
@@ -818,6 +822,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.boolean "build_events", default: false, null: false
t.string "category", default: "common", null: false
t.boolean "default", default: false
+ t.boolean "wiki_page_events", default: true
end
add_index "services", ["category"], name: "index_services_on_category", using: :btree
@@ -1012,6 +1017,7 @@ ActiveRecord::Schema.define(version: 20160412140240) do
t.boolean "note_events", default: false, null: false
t.boolean "enable_ssl_verification", default: true
t.boolean "build_events", default: false, null: false
+ t.boolean "wiki_page_events", default: false, null: false
end
add_index "web_hooks", ["created_at", "id"], name: "index_web_hooks_on_created_at_and_id", using: :btree
diff --git a/doc/api/README.md b/doc/api/README.md
index 3a8fa6cebd1..ff039f1886f 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -33,6 +33,7 @@ following locations:
- [Build triggers](build_triggers.md)
- [Build Variables](build_variables.md)
- [Runners](runners.md)
+- [Licenses](licenses.md)
## Authentication
diff --git a/doc/api/licenses.md b/doc/api/licenses.md
new file mode 100644
index 00000000000..855b0eab56f
--- /dev/null
+++ b/doc/api/licenses.md
@@ -0,0 +1,147 @@
+# Licenses
+
+## List license templates
+
+Get all license templates.
+
+```
+GET /licenses
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ------- | -------- | --------------------- |
+| `popular` | boolean | no | If passed, returns only popular licenses |
+
+```bash
+curl https://gitlab.example.com/api/v3/licenses?popular=1
+```
+
+Example response:
+
+```json
+[
+ {
+ "key": "apache-2.0",
+ "name": "Apache License 2.0",
+ "nickname": null,
+ "featured": true,
+ "html_url": "http://choosealicense.com/licenses/apache-2.0/",
+ "source_url": "http://www.apache.org/licenses/LICENSE-2.0.html",
+ "description": "A permissive license that also provides an express grant of patent rights from contributors to users.",
+ "conditions": [
+ "include-copyright",
+ "document-changes"
+ ],
+ "permissions": [
+ "commercial-use",
+ "modifications",
+ "distribution",
+ "patent-use",
+ "private-use"
+ ],
+ "limitations": [
+ "trademark-use",
+ "no-liability"
+ ],
+ "content": " Apache License\n Version 2.0, January 2004\n [...]"
+ },
+ {
+ "key": "gpl-3.0",
+ "name": "GNU General Public License v3.0",
+ "nickname": "GNU GPLv3",
+ "featured": true,
+ "html_url": "http://choosealicense.com/licenses/gpl-3.0/",
+ "source_url": "http://www.gnu.org/licenses/gpl-3.0.txt",
+ "description": "The GNU GPL is the most widely used free software license and has a strong copyleft requirement. When distributing derived works, the source code of the work must be made available under the same license.",
+ "conditions": [
+ "include-copyright",
+ "document-changes",
+ "disclose-source",
+ "same-license"
+ ],
+ "permissions": [
+ "commercial-use",
+ "modifications",
+ "distribution",
+ "patent-use",
+ "private-use"
+ ],
+ "limitations": [
+ "no-liability"
+ ],
+ "content": " GNU GENERAL PUBLIC LICENSE\n Version 3, 29 June 2007\n [...]"
+ },
+ {
+ "key": "mit",
+ "name": "MIT License",
+ "nickname": null,
+ "featured": true,
+ "html_url": "http://choosealicense.com/licenses/mit/",
+ "source_url": "http://opensource.org/licenses/MIT",
+ "description": "A permissive license that is short and to the point. It lets people do anything with your code with proper attribution and without warranty.",
+ "conditions": [
+ "include-copyright"
+ ],
+ "permissions": [
+ "commercial-use",
+ "modifications",
+ "distribution",
+ "private-use"
+ ],
+ "limitations": [
+ "no-liability"
+ ],
+ "content": "The MIT License (MIT)\n\nCopyright (c) [year] [fullname]\n [...]"
+ }
+]
+```
+
+## Single license template
+
+Get a single license template. You can pass parameters to replace the license
+placeholder.
+
+```
+GET /licenses/:key
+```
+
+| Attribute | Type | Required | Description |
+| ---------- | ------ | -------- | ----------- |
+| `key` | string | yes | The key of the license template |
+| `project` | string | no | The copyrighted project name |
+| `fullname` | string | no | The full-name of the copyright holder |
+
+>**Note:**
+If you omit the `fullname` parameter but authenticate your request, the name of
+the authenticated user will be used to replace the copyright holder placeholder.
+
+```bash
+curl -H "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/licenses/mit?project=My+Cool+Project
+```
+
+Example response:
+
+```json
+{
+ "key": "mit",
+ "name": "MIT License",
+ "nickname": null,
+ "featured": true,
+ "html_url": "http://choosealicense.com/licenses/mit/",
+ "source_url": "http://opensource.org/licenses/MIT",
+ "description": "A permissive license that is short and to the point. It lets people do anything with your code with proper attribution and without warranty.",
+ "conditions": [
+ "include-copyright"
+ ],
+ "permissions": [
+ "commercial-use",
+ "modifications",
+ "distribution",
+ "private-use"
+ ],
+ "limitations": [
+ "no-liability"
+ ],
+ "content": "The MIT License (MIT)\n\nCopyright (c) 2016 John Doe\n [...]"
+}
+```
diff --git a/doc/ci/api/builds.md b/doc/ci/api/builds.md
index d100e261178..79761a893da 100644
--- a/doc/ci/api/builds.md
+++ b/doc/ci/api/builds.md
@@ -26,48 +26,114 @@ This API uses two types of authentication:
### Runs oldest pending build by runner
- POST /ci/api/v1/builds/register
+```
+POST /ci/api/v1/builds/register
+```
-Parameters:
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|---------------------|
+| `token` | string | yes | Unique runner token |
- * `token` (required) - Unique runner token
+```
+curl -X POST "https://gitlab.example.com/ci/api/v1/builds/register" -F "token=t0k3n"
+```
### Update details of an existing build
- PUT /ci/api/v1/builds/:id
+```
+PUT /ci/api/v1/builds/:id
+```
+
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|----------------------|
+| `id` | integer | yes | The ID of a project |
+| `token` | string | yes | Unique runner token |
+| `state` | string | no | The state of a build |
+| `trace` | string | no | The trace of a build |
+
+```
+curl -X PUT "https://gitlab.example.com/ci/api/v1/builds/1234" -F "token=t0k3n" -F "state=running" -F "trace=Running git clone...\n"
+```
+
+### Incremental build trace update
+
+Using this method you need to send trace content as a request body. You also need to provide the `Content-Range` header
+with a range of sent trace part. Note that you need to send parts in the proper order, so the begining of the part
+must start just after the end of the previous part. If you provide the wrong part, then GitLab CI API will return `416
+Range Not Satisfiable` response with a header `Range: 0-X`, where `X` is the current trace length.
+
+For example, if you receive `Range: 0-11` in the response, then your next part must contain a `Content-Range: 11-...`
+header and a trace part covered by this range.
+
+For a valid update API will return `202` response with:
+* `Build-Status: {status}` header containing current status of the build,
+* `Range: 0-{length}` header with the current trace length.
+
+```
+PATCH /ci/api/v1/builds/:id/trace.txt
+```
Parameters:
- * `id` (required) - The ID of a project
- * `token` (required) - Unique runner token
- * `state` (optional) - The state of a build
- * `trace` (optional) - The trace of a build
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|----------------------|
+| `id` | integer | yes | The ID of a build |
+
+Headers:
+
+| Attribute | Type | Required | Description |
+|-----------------|---------|----------|-----------------------------------|
+| `BUILD-TOKEN` | string | yes | The build authorization token |
+| `Content-Range` | string | yes | Bytes range of trace that is sent |
+
+```
+curl -X PATCH "https://gitlab.example.com/ci/api/v1/builds/1234/trace.txt" -H "BUILD-TOKEN=build_t0k3n" -H "Content-Range=0-21" -d "Running git clone...\n"
+```
+
### Upload artifacts to build
- POST /ci/api/v1/builds/:id/artifacts
+```
+POST /ci/api/v1/builds/:id/artifacts
+```
-Parameters:
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| `id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
+| `file` | mixed | yes | Artifacts file |
- * `id` (required) - The ID of a build
- * `token` (required) - The build authorization token
- * `file` (required) - Artifacts file
+```
+curl -X POST "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n" -F "file=@/path/to/file"
+```
### Download the artifacts file from build
- GET /ci/api/v1/builds/:id/artifacts
+```
+GET /ci/api/v1/builds/:id/artifacts
+```
-Parameters:
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| `id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
- * `id` (required) - The ID of a build
- * `token` (required) - The build authorization token
+```
+curl "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+```
### Remove the artifacts file from build
- DELETE /ci/api/v1/builds/:id/artifacts
+```
+DELETE /ci/api/v1/builds/:id/artifacts
+```
-Parameters:
+| Attribute | Type | Required | Description |
+|-----------|---------|----------|-------------------------------|
+| ` id` | integer | yes | The ID of a build |
+| `token` | string | yes | The build authorization token |
- * ` id` (required) - The ID of a build
- * `token` (required) - The build authorization token
+```
+curl -X DELETE "https://gitlab.example.com/ci/api/v1/builds/1234/artifacts" -F "token=build_t0k3n"
+```
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index c7df0713a3d..a06650b3387 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -7,6 +7,10 @@ through the coordinator API of GitLab CI.
A runner can be specific to a certain project or serve any project
in GitLab CI. A runner that serves all projects is called a shared runner.
+Ideally, GitLab Runner should not be installed on the same machine as GitLab.
+Read the [requirements documentation](../../install/requirements.md#gitlab-runner)
+for more information.
+
## Shared vs. Specific Runners
A runner that is specific only runs for the specified project. A shared runner
@@ -140,7 +144,7 @@ to it. This means that if you have shared runners setup for a project and
someone forks that project, the shared runners will also serve jobs of this
project.
-# Attack vectors in runners
+## Attack vectors in Runners
Mentioned briefly earlier, but the following things of runners can be exploited.
We're always looking for contributions that can mitigate these [Security Considerations](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md).
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 61475b45988..7e9bced7616 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -15,6 +15,7 @@ If you want a quick introduction to GitLab CI, follow our
- [.gitlab-ci.yml](#gitlab-ci-yml)
- [image and services](#image-and-services)
- [before_script](#before_script)
+ - [after_script](#after_script)
- [stages](#stages)
- [types](#types)
- [variables](#variables)
@@ -30,6 +31,7 @@ If you want a quick introduction to GitLab CI, follow our
- [artifacts](#artifacts)
- [artifacts:name](#artifacts-name)
- [dependencies](#dependencies)
+ - [before_script and after_script](#before_script-and-after_script)
- [Hidden jobs](#hidden-jobs)
- [Special YAML features](#special-yaml-features)
- [Anchors](#anchors)
@@ -81,6 +83,9 @@ services:
before_script:
- bundle install
+after_script:
+ - rm secrets
+
stages:
- build
- test
@@ -105,6 +110,7 @@ There are a few reserved `keywords` that **cannot** be used as job names:
| stages | no | Define build stages |
| types | no | Alias for `stages` |
| before_script | no | Define commands that run before each job's script |
+| after_script | no | Define commands that run after each job's script |
| variables | no | Define build variables |
| cache | no | Define list of files that should be cached between subsequent runs |
@@ -119,6 +125,14 @@ used for time of the build. The configuration of this feature is covered in
`before_script` is used to define the command that should be run before all
builds, including deploy builds. This can be an array or a multi-line string.
+### after_script
+
+>**Note:**
+Introduced in GitLab 8.7 and GitLab Runner v1.2.
+
+`after_script` is used to define the command that will be run after for all
+builds. This has to be an array or a multi-line string.
+
### stages
`stages` is used to define build stages that can be used by jobs.
@@ -336,6 +350,8 @@ job_name:
| dependencies | no | Define other builds that a build depends on so that you can pass artifacts between them|
| artifacts | no | Define list build artifacts |
| cache | no | Define list of files that should be cached between subsequent runs |
+| before_script | no | Override a set of commands that are executed before build |
+| after_script | no | Override a set of commands that are executed after build |
### script
@@ -692,6 +708,23 @@ deploy:
script: make deploy
```
+### before_script and after_script
+
+It's possible to overwrite globally defined `before_script` and `after_script`:
+
+```yaml
+before_script
+- global before script
+
+job:
+ before_script:
+ - execute this instead of global before script
+ script:
+ - my command
+ after_script:
+ - execute this after my script
+```
+
## Hidden jobs
>**Note:**
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 03cb08dd1f1..eb9fe5e1b1b 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -79,6 +79,26 @@ With less memory GitLab will give strange errors during the reconfigure run and
Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as top or htop) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about many you need of those.
+## Gitlab Runner
+
+We strongly advise against installing GitLab Runner on the same machine you plan
+to install GitLab on. Depending on how you decide to configure GitLab Runner and
+what tools you use to exercise your application in the CI environment, GitLab
+Runner can consume significant amount of available memory.
+
+Memory consumption calculations, that are available above, will not be valid if
+you decide to run GitLab Runner and the GitLab Rails application on the same
+machine.
+
+It is also not safe to install everything on a single machine, because of the
+[security reasons] - especially when you plan to use shell executor with GitLab
+Runner.
+
+We recommend using a separate machine for each GitLab Runner, if you plan to
+use the CI features.
+
+[security reasons]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/security/index.md
+
## Unicorn Workers
It's possible to increase the amount of unicorn workers and this will usually help for to reduce the response time of the applications and increase the ability to handle parallel requests.
diff --git a/doc/intro/README.md b/doc/intro/README.md
index fecbbe6317b..ab298d3808e 100644
--- a/doc/intro/README.md
+++ b/doc/intro/README.md
@@ -25,6 +25,7 @@ Create merge requests and review code.
- [Automatically close issues from merge requests](../customization/issue_closing.md)
- [Automatically merge when your builds succeed](../workflow/merge_when_build_succeeds.md)
- [Revert any commit](../workflow/revert_changes.md)
+- [Cherry-pick any commit](../workflow/cherry_pick_changes.md)
## Test and Deploy
diff --git a/doc/system_hooks/system_hooks.md b/doc/system_hooks/system_hooks.md
index 612376e3a49..c44930a4ceb 100644
--- a/doc/system_hooks/system_hooks.md
+++ b/doc/system_hooks/system_hooks.md
@@ -4,6 +4,12 @@ Your GitLab instance can perform HTTP POST requests on the following events: `pr
System hooks can be used, e.g. for logging or changing information in a LDAP server.
+> **Note:**
+>
+> We follow the same structure from Webhooks for Push and Tag events, but we never display commits.
+>
+> Same deprecations from Webhooks are valid here.
+
## Hooks request example
**Request header**:
@@ -240,3 +246,110 @@ X-Gitlab-Event: System Hook
"user_id": 41
}
```
+
+## Push events
+
+Triggered when you push to the repository except when pushing tags.
+
+**Request header**:
+
+```
+X-Gitlab-Event: System Hook
+```
+
+**Request body:**
+
+```json
+{
+ "event_name": "push",
+ "before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
+ "after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "ref": "refs/heads/master",
+ "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
+ "user_id": 4,
+ "user_name": "John Smith",
+ "user_email": "john@example.com",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 15,
+ "project":{
+ "name":"Diaspora",
+ "description":"",
+ "web_url":"http://example.com/mike/diaspora",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "namespace":"Mike",
+ "visibility_level":0,
+ "path_with_namespace":"mike/diaspora",
+ "default_branch":"master",
+ "homepage":"http://example.com/mike/diaspora",
+ "url":"git@example.com:mike/diaspora.git",
+ "ssh_url":"git@example.com:mike/diaspora.git",
+ "http_url":"http://example.com/mike/diaspora.git"
+ },
+ "repository":{
+ "name": "Diaspora",
+ "url": "git@example.com:mike/diaspora.git",
+ "description": "",
+ "homepage": "http://example.com/mike/diaspora",
+ "git_http_url":"http://example.com/mike/diaspora.git",
+ "git_ssh_url":"git@example.com:mike/diaspora.git",
+ "visibility_level":0
+ },
+ "commits": [],
+ "total_commits_count": 0
+}
+```
+
+## Tag events
+
+Triggered when you create (or delete) tags to the repository.
+
+**Request header**:
+
+```
+X-Gitlab-Event: System Hook
+```
+
+**Request body:**
+
+```json
+{
+ "event_name": "tag_push",
+ "before": "0000000000000000000000000000000000000000",
+ "after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "ref": "refs/tags/v1.0.0",
+ "checkout_sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
+ "user_id": 1,
+ "user_name": "John Smith",
+ "user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
+ "project_id": 1,
+ "project":{
+ "name":"Example",
+ "description":"",
+ "web_url":"http://example.com/jsmith/example",
+ "avatar_url":null,
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "namespace":"Jsmith",
+ "visibility_level":0,
+ "path_with_namespace":"jsmith/example",
+ "default_branch":"master",
+ "homepage":"http://example.com/jsmith/example",
+ "url":"git@example.com:jsmith/example.git",
+ "ssh_url":"git@example.com:jsmith/example.git",
+ "http_url":"http://example.com/jsmith/example.git"
+ },
+ "repository":{
+ "name": "Example",
+ "url": "ssh://git@example.com/jsmith/example.git",
+ "description": "",
+ "homepage": "http://example.com/jsmith/example",
+ "git_http_url":"http://example.com/jsmith/example.git",
+ "git_ssh_url":"git@example.com:jsmith/example.git",
+ "visibility_level":0
+ },
+ "commits": [],
+ "total_commits_count": 0
+}
+```
diff --git a/doc/web_hooks/web_hooks.md b/doc/web_hooks/web_hooks.md
index 22e207b6d32..c1c51302e79 100644
--- a/doc/web_hooks/web_hooks.md
+++ b/doc/web_hooks/web_hooks.md
@@ -41,6 +41,7 @@ X-Gitlab-Event: Push Hook
"before": "95790bf891e76fee5e1747ab589903a6a1f80f22",
"after": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"ref": "refs/heads/master",
+ "checkout_sha": "da1560886d4f094c3e6c9ef40349f7d38b5d27d7",
"user_id": 4,
"user_name": "John Smith",
"user_email": "john@example.com",
@@ -118,9 +119,10 @@ X-Gitlab-Event: Tag Push Hook
```json
{
"object_kind": "tag_push",
- "ref": "refs/tags/v1.0.0",
"before": "0000000000000000000000000000000000000000",
"after": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
+ "ref": "refs/tags/v1.0.0",
+ "checkout_sha": "82b3d5ae55f7080f1e6022629cdb57bfae7cccc7",
"user_id": 1,
"user_name": "John Smith",
"user_avatar": "https://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=8://s.gravatar.com/avatar/d4c74594d841139328695756648b6bd6?s=80",
diff --git a/doc/workflow/README.md b/doc/workflow/README.md
index 25893f948ea..9efe41308dc 100644
--- a/doc/workflow/README.md
+++ b/doc/workflow/README.md
@@ -20,6 +20,7 @@
- [Milestones](milestones.md)
- [Merge Requests](merge_requests.md)
- [Revert changes](revert_changes.md)
+- [Cherry-pick changes](cherry_pick_changes.md)
- ["Work In Progress" Merge Requests](wip_merge_requests.md)
- [Merge When Build Succeeds](merge_when_build_succeeds.md)
- [Manage large binaries with Git LFS](lfs/manage_large_binaries_with_git_lfs.md)
diff --git a/doc/workflow/cherry_pick_changes.md b/doc/workflow/cherry_pick_changes.md
new file mode 100644
index 00000000000..b0ca0879643
--- /dev/null
+++ b/doc/workflow/cherry_pick_changes.md
@@ -0,0 +1,52 @@
+# Cherry-pick changes
+
+_**Note:** This feature was [introduced][ce-3514] in GitLab 8.7._
+
+---
+
+GitLab implements Git's powerful feature to [cherry-pick any commit][git-cherry-pick]
+with introducing a **Cherry-pick** button in Merge Requests and commit details.
+
+## Cherry-picking a Merge Request
+
+After the Merge Request has been merged, a **Cherry-pick** button will be available
+to cherry-pick the changes introduced by that Merge Request:
+
+![Cherry-pick Merge Request](img/cherry_pick_changes_mr.png)
+
+---
+
+You can cherry-pick the changes directly into the selected branch or you can opt to
+create a new Merge Request with the cherry-pick changes:
+
+![Cherry-pick Merge Request modal](img/cherry_pick_changes_mr_modal.png)
+
+## Cherry-picking a Commit
+
+You can cherry-pick a Commit from the Commit details page:
+
+![Cherry-pick commit](img/cherry_pick_changes_commit.png)
+
+---
+
+Similar to cherry-picking a Merge Request, you can opt to cherry-pick the changes
+directly into the target branch or create a new Merge Request to cherry-pick the
+changes:
+
+![Cherry-pick commit modal](img/cherry_pick_changes_commit_modal.png)
+
+---
+
+Please note that when cherry-picking merge commits, the mainline will always be the
+first parent. If you want to use a different mainline then you need to do that
+from the command line.
+
+Here is a quick example to cherry-pick a merge commit using the second parent as the
+mainline:
+
+```bash
+git cherry-pick -m 2 7a39eb0
+```
+
+[ce-3514]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/3514 "Cherry-pick button Merge Request"
+[git-cherry-pick]: https://git-scm.com/docs/git-cherry-pick "Git cherry-pick documentation"
diff --git a/doc/workflow/img/cherry_pick_changes_commit.png b/doc/workflow/img/cherry_pick_changes_commit.png
new file mode 100644
index 00000000000..ae91d2cae53
--- /dev/null
+++ b/doc/workflow/img/cherry_pick_changes_commit.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_commit_modal.png b/doc/workflow/img/cherry_pick_changes_commit_modal.png
new file mode 100644
index 00000000000..f502f87677a
--- /dev/null
+++ b/doc/workflow/img/cherry_pick_changes_commit_modal.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr.png b/doc/workflow/img/cherry_pick_changes_mr.png
new file mode 100644
index 00000000000..59c610e620b
--- /dev/null
+++ b/doc/workflow/img/cherry_pick_changes_mr.png
Binary files differ
diff --git a/doc/workflow/img/cherry_pick_changes_mr_modal.png b/doc/workflow/img/cherry_pick_changes_mr_modal.png
new file mode 100644
index 00000000000..96a80f4726d
--- /dev/null
+++ b/doc/workflow/img/cherry_pick_changes_mr_modal.png
Binary files differ
diff --git a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
index ba91685a20b..31620044b15 100644
--- a/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
+++ b/doc/workflow/lfs/manage_large_binaries_with_git_lfs.md
@@ -44,7 +44,7 @@ check it into your Git repository:
```bash
git clone git@gitlab.example.com:group/project.git
-git lfs init # initialize the Git LFS project project
+git lfs install # initialize the Git LFS project project
git lfs track "*.iso" # select the file extensions that you want to treat as large files
```
diff --git a/features/project/issues/filter_labels.feature b/features/project/issues/filter_labels.feature
index e07f8053fb7..49d7a3b9af2 100644
--- a/features/project/issues/filter_labels.feature
+++ b/features/project/issues/filter_labels.feature
@@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels
@javascript
Scenario: I filter by one label
Given I click link "bug"
+ And I click "dropdown close button"
Then I should see "Bugfix1" in issues list
And I should see "Bugfix2" in issues list
And I should not see "Feature1" in issues list
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index 1e09dbc4c8f..fdffd71de85 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -124,19 +124,6 @@ Feature: Project Source Browse Files
And I can see the replacement commit message
@javascript
- Scenario: I can create file in empty repo
- Given I own an empty project
- And I visit my empty project page
- And I create bare repo
- When I click on "add a file" link
- And I edit code
- And I fill the new file name
- And I fill the commit message
- And I click on "Commit Changes"
- Then I am redirected to the new file
- And I should see its new content
-
- @javascript
Scenario: If I enter an illegal file name I see an error message
Given I click on "New file" link in repo
And I fill the new file name with an illegal name
diff --git a/features/steps/dashboard/todos.rb b/features/steps/dashboard/todos.rb
index a6e574f12a9..2b23df6764b 100644
--- a/features/steps/dashboard/todos.rb
+++ b/features/steps/dashboard/todos.rb
@@ -31,7 +31,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
expect(page).to have_content 'Done 0'
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title)
should_see_todo(2, "John Doe mentioned you on issue ##{issue.iid}", "#{current_user.to_reference} Wdyt?")
should_see_todo(3, "John Doe assigned you issue ##{issue.iid}", issue.title)
should_see_todo(4, "Mary Jane mentioned you on issue ##{issue.iid}", issue.title)
@@ -45,7 +45,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
page.within('.nav-sidebar') { expect(page).to have_content 'Todos 3' }
expect(page).to have_content 'To do 3'
expect(page).to have_content 'Done 1'
- should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
end
step 'I click on the "Done" tab' do
@@ -54,7 +54,7 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
step 'I should see all todos marked as done' do
expect(page).to have_link project.name_with_namespace
- should_see_todo(1, "John Doe assigned you merge request !#{merge_request.iid}", merge_request.title, false)
+ should_see_todo(1, "John Doe assigned you merge request #{merge_request.to_reference}", merge_request.title, false)
end
step 'I filter by "Enterprise"' do
@@ -82,11 +82,11 @@ class Spinach::Features::DashboardTodos < Spinach::FeatureSteps
end
step 'I should not see todos related to "Merge Requests" in the list' do
- should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
end
step 'I should not see todos related to "Assignments" in the list' do
- should_not_see_todo "John Doe assigned you merge request !#{merge_request.iid}"
+ should_not_see_todo "John Doe assigned you merge request #{merge_request.to_reference}"
should_not_see_todo "John Doe assigned you issue ##{issue.iid}"
end
diff --git a/features/steps/project/commits/user_lookup.rb b/features/steps/project/commits/user_lookup.rb
index 40cada6da45..2d43be5a386 100644
--- a/features/steps/project/commits/user_lookup.rb
+++ b/features/steps/project/commits/user_lookup.rb
@@ -29,8 +29,9 @@ class Spinach::Features::ProjectCommitsUserLookup < Spinach::FeatureSteps
def check_author_link(email, user)
author_link = find('.commit-author-link')
+
expect(author_link['href']).to eq user_path(user)
- expect(author_link['data-original-title']).to eq email
+ expect(author_link['title']).to eq email
expect(find('.commit-author-name').text).to eq user.name
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index 612bb8fd8b1..0ead83d6937 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -114,7 +114,7 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
step 'I see the edit page prefilled for "Merge Request On Forked Project"' do
expect(current_path).to eq edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- expect(page).to have_content "Edit merge request ##{@merge_request.id}"
+ expect(page).to have_content "Edit merge request #{@merge_request.to_reference}"
expect(find("#merge_request_title").value).to eq "Merge Request On Forked Project"
end
diff --git a/features/steps/project/issues/filter_labels.rb b/features/steps/project/issues/filter_labels.rb
index 6d50501a722..d82c6856918 100644
--- a/features/steps/project/issues/filter_labels.rb
+++ b/features/steps/project/issues/filter_labels.rb
@@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
page.find('.js-label-select').click
sleep 0.5
execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
+ end
+
+ step 'I click "dropdown close button"' do
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index e072505e5d7..c26d7a15212 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -282,8 +282,8 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_link 'Create empty bare repository'
end
- step 'I click on "add a file" link' do
- click_link 'adding README'
+ step 'I click on "README" link' do
+ click_link 'README'
# Remove pre-receive hook so we can push without auth
FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive'))
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 7d65145176b..cc1004f8005 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -57,5 +57,6 @@ module API
mount Builds
mount Variables
mount Runners
+ mount Licenses
end
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 60b9f5e0ece..716ca6f7ed9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -439,5 +439,17 @@ module API
class Variable < Grape::Entity
expose :key, :value
end
+
+ class RepoLicense < Grape::Entity
+ expose :key, :name, :nickname
+ expose :featured, as: :popular
+ expose :url, as: :html_url
+ expose(:source_url) { |license| license.meta['source'] }
+ expose(:description) { |license| license.meta['description'] }
+ expose(:conditions) { |license| license.meta['conditions'] }
+ expose(:permissions) { |license| license.meta['permissions'] }
+ expose(:limitations) { |license| license.meta['limitations'] }
+ expose :content
+ end
end
end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 8cfa1f1556b..3ac7b50c4ce 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -23,7 +23,7 @@ module API
end
post "/allowed" do
- Gitlab::Metrics.tag_transaction('action', 'Grape#/internal/allowed')
+ Gitlab::Metrics.action = 'Grape#/internal/allowed'
status 200
diff --git a/lib/api/licenses.rb b/lib/api/licenses.rb
new file mode 100644
index 00000000000..187d2c04703
--- /dev/null
+++ b/lib/api/licenses.rb
@@ -0,0 +1,58 @@
+module API
+ # Licenses API
+ class Licenses < Grape::API
+ PROJECT_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (project|description|
+ one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
+ [\>\}\]]/xi.freeze
+ YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
+ FULLNAME_TEMPLATE_REGEX =
+ /[\<\{\[]
+ (fullname|name\sof\s(author|copyright\sowner))
+ [\>\}\]]/xi.freeze
+
+ # Get the list of the available license templates
+ #
+ # Parameters:
+ # popular - Filter licenses to only the popular ones
+ #
+ # Example Request:
+ # GET /licenses
+ # GET /licenses?popular=1
+ get 'licenses' do
+ options = {
+ featured: params[:popular].present? ? true : nil
+ }
+ present Licensee::License.all(options), with: Entities::RepoLicense
+ end
+
+ # Get text for specific license
+ #
+ # Parameters:
+ # key (required) - The key of a license
+ # project - Copyrighted project name
+ # fullname - Full name of copyright holder
+ #
+ # Example Request:
+ # GET /licenses/mit
+ #
+ get 'licenses/:key', requirements: { key: /[\w\.-]+/ } do
+ required_attributes! [:key]
+
+ not_found!('License') unless Licensee::License.find(params[:key])
+
+ # We create a fresh Licensee::License object since we'll modify its
+ # content in place below.
+ license = Licensee::License.new(params[:key])
+
+ license.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
+ license.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
+
+ fullname = params[:fullname].presence || current_user.try(:name)
+ license.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
+
+ present license, with: Entities::RepoLicense
+ end
+ end
+end
diff --git a/lib/api/tags.rb b/lib/api/tags.rb
index d1a10479e44..3e1ed3fe5c7 100644
--- a/lib/api/tags.rb
+++ b/lib/api/tags.rb
@@ -12,7 +12,7 @@ module API
# Example Request:
# GET /projects/:id/repository/tags
get ":id/repository/tags" do
- present user_project.repo.tags.sort_by(&:name).reverse,
+ present user_project.repository.tags.sort_by(&:name).reverse,
with: Entities::RepoTag, project: user_project
end
diff --git a/lib/ci/api/api.rb b/lib/ci/api/api.rb
index 4e85d2c3c74..353c4ddebf8 100644
--- a/lib/ci/api/api.rb
+++ b/lib/ci/api/api.rb
@@ -23,6 +23,8 @@ module Ci
rack_response({ 'message' => '500 Internal Server Error' }, 500)
end
+ content_type :txt, 'text/plain'
+ content_type :json, 'application/json'
format :json
helpers ::Ci::API::Helpers
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 2e9a5d311f9..607359769d1 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -50,6 +50,39 @@ module Ci
end
end
+ # Send incremental log update - Runners only
+ #
+ # Parameters:
+ # id (required) - The ID of a build
+ # Body:
+ # content of logs to append
+ # Headers:
+ # Content-Range (required) - range of content that was sent
+ # BUILD-TOKEN (required) - The build authorization token
+ # Example Request:
+ # PATCH /builds/:id/trace.txt
+ patch ":id/trace.txt" do
+ build = Ci::Build.find_by_id(params[:id])
+ not_found! unless build
+ authenticate_build_token!(build)
+ forbidden!('Build has been erased!') if build.erased?
+
+ error!('400 Missing header Content-Range', 400) unless request.headers.has_key?('Content-Range')
+ content_range = request.headers['Content-Range']
+ content_range = content_range.split('-')
+
+ current_length = build.trace_length
+ unless current_length == content_range[0].to_i
+ return error!('416 Range Not Satisfiable', 416, { 'Range' => "0-#{current_length}" })
+ end
+
+ build.append_trace(request.body.read, content_range[0].to_i)
+
+ status 202
+ header 'Build-Status', build.status
+ header 'Range', "0-#{build.trace_length}"
+ end
+
# Authorize artifacts uploading for build - Runners only
#
# Parameters:
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index b8ede3a7edc..ff9887cba1e 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -4,12 +4,12 @@ module Ci
DEFAULT_STAGES = %w(build test deploy)
DEFAULT_STAGE = 'test'
- ALLOWED_YAML_KEYS = [:before_script, :image, :services, :types, :stages, :variables, :cache]
+ ALLOWED_YAML_KEYS = [:before_script, :after_script, :image, :services, :types, :stages, :variables, :cache]
ALLOWED_JOB_KEYS = [:tags, :script, :only, :except, :type, :image, :services,
:allow_failure, :type, :stage, :when, :artifacts, :cache,
- :dependencies, :variables]
+ :dependencies, :before_script, :after_script, :variables]
- attr_reader :before_script, :image, :services, :path, :cache
+ attr_reader :before_script, :after_script, :image, :services, :path, :cache
def initialize(config, path = nil)
@config = YAML.safe_load(config, [Symbol], [], true)
@@ -55,6 +55,7 @@ module Ci
def initial_parsing
@before_script = @config[:before_script] || []
+ @after_script = @config[:after_script]
@image = @config[:image]
@services = @config[:services]
@stages = @config[:stages] || @config[:types]
@@ -83,7 +84,7 @@ module Ci
{
stage_idx: stages.index(job[:stage]),
stage: job[:stage],
- commands: "#{@before_script.join("\n")}\n#{normalize_script(job[:script])}",
+ commands: [job[:before_script] || @before_script, job[:script]].flatten.join("\n"),
tag_list: job[:tags] || [],
name: name,
only: job[:only],
@@ -96,23 +97,32 @@ module Ci
artifacts: job[:artifacts],
cache: job[:cache] || @cache,
dependencies: job[:dependencies],
+ after_script: job[:after_script] || @after_script,
}.compact
}
end
- def normalize_script(script)
- if script.is_a? Array
- script.join("\n")
- else
- script
+ def validate!
+ validate_global!
+
+ @jobs.each do |name, job|
+ validate_job!(name, job)
end
+
+ true
end
- def validate!
+ private
+
+ def validate_global!
unless validate_array_of_strings(@before_script)
raise ValidationError, "before_script should be an array of strings"
end
+ unless @after_script.nil? || validate_array_of_strings(@after_script)
+ raise ValidationError, "after_script should be an array of strings"
+ end
+
unless @image.nil? || @image.is_a?(String)
raise ValidationError, "image should be a string"
end
@@ -129,31 +139,28 @@ module Ci
raise ValidationError, "variables should be a map of key-value strings"
end
- if @cache
- if @cache[:key] && !validate_string(@cache[:key])
- raise ValidationError, "cache:key parameter should be a string"
- end
-
- if @cache[:untracked] && !validate_boolean(@cache[:untracked])
- raise ValidationError, "cache:untracked parameter should be an boolean"
- end
+ validate_global_cache! if @cache
+ end
- if @cache[:paths] && !validate_array_of_strings(@cache[:paths])
- raise ValidationError, "cache:paths parameter should be an array of strings"
- end
+ def validate_global_cache!
+ if @cache[:key] && !validate_string(@cache[:key])
+ raise ValidationError, "cache:key parameter should be a string"
end
- @jobs.each do |name, job|
- validate_job!(name, job)
+ if @cache[:untracked] && !validate_boolean(@cache[:untracked])
+ raise ValidationError, "cache:untracked parameter should be an boolean"
end
- true
+ if @cache[:paths] && !validate_array_of_strings(@cache[:paths])
+ raise ValidationError, "cache:paths parameter should be an array of strings"
+ end
end
def validate_job!(name, job)
validate_job_name!(name)
validate_job_keys!(name, job)
validate_job_types!(name, job)
+ validate_job_script!(name, job)
validate_job_stage!(name, job) if job[:stage]
validate_job_variables!(name, job) if job[:variables]
@@ -162,8 +169,6 @@ module Ci
validate_job_dependencies!(name, job) if job[:dependencies]
end
- private
-
def validate_job_name!(name)
if name.blank? || !validate_string(name)
raise ValidationError, "job name should be non-empty string"
@@ -179,10 +184,6 @@ module Ci
end
def validate_job_types!(name, job)
- if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
- raise ValidationError, "#{name} job: script should be a string or an array of a strings"
- end
-
if job[:image] && !validate_string(job[:image])
raise ValidationError, "#{name} job: image should be a string"
end
@@ -212,6 +213,20 @@ module Ci
end
end
+ def validate_job_script!(name, job)
+ if !validate_string(job[:script]) && !validate_array_of_strings(job[:script])
+ raise ValidationError, "#{name} job: script should be a string or an array of a strings"
+ end
+
+ if job[:before_script] && !validate_array_of_strings(job[:before_script])
+ raise ValidationError, "#{name} job: before_script should be an array of strings"
+ end
+
+ if job[:after_script] && !validate_array_of_strings(job[:after_script])
+ raise ValidationError, "#{name} job: after_script should be an array of strings"
+ end
+ end
+
def validate_job_stage!(name, job)
unless job[:stage].is_a?(String) && job[:stage].in?(stages)
raise ValidationError, "#{name} job: stage parameter should be #{stages.join(", ")}"
diff --git a/lib/file_size_validator.rb b/lib/file_size_validator.rb
index 2eae55e534b..440dd44ece7 100644
--- a/lib/file_size_validator.rb
+++ b/lib/file_size_validator.rb
@@ -1,9 +1,9 @@
class FileSizeValidator < ActiveModel::EachValidator
- MESSAGES = { is: :wrong_size, minimum: :size_too_small, maximum: :size_too_big }.freeze
- CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
+ MESSAGES = { is: :wrong_size, minimum: :size_too_small, maximum: :size_too_big }.freeze
+ CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
- DEFAULT_TOKENIZER = lambda { |value| value.split(//) }
- RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
+ DEFAULT_TOKENIZER = -> (value) { value.split(//) }.freeze
+ RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long].freeze
def initialize(options)
if range = (options.delete(:in) || options.delete(:within))
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index 5ebaad6ca6e..eb27d82f110 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -7,6 +7,7 @@ module Gitlab
gon.max_file_size = current_application_settings.max_attachment_size
gon.relative_url_root = Gitlab.config.gitlab.relative_url_root
gon.user_color_scheme = Gitlab::ColorSchemes.for_user(current_user).css_class
+ gon.sentry_dsn = ApplicationSetting.current.sentry_dsn if Rails.env.production?
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/metrics.rb b/lib/gitlab/metrics.rb
index 484970c5a10..49f702f91f6 100644
--- a/lib/gitlab/metrics.rb
+++ b/lib/gitlab/metrics.rb
@@ -14,7 +14,8 @@ module Gitlab
method_call_threshold: current_application_settings[:metrics_method_call_threshold],
host: current_application_settings[:metrics_host],
port: current_application_settings[:metrics_port],
- sample_interval: current_application_settings[:metrics_sample_interval] || 15
+ sample_interval: current_application_settings[:metrics_sample_interval] || 15,
+ packet_size: current_application_settings[:metrics_packet_size] || 1
}
end
@@ -41,9 +42,9 @@ module Gitlab
prepared = prepare_metrics(metrics)
pool.with do |connection|
- prepared.each do |metric|
+ prepared.each_slice(settings[:packet_size]) do |slice|
begin
- connection.write_points([metric])
+ connection.write_points(slice)
rescue StandardError
end
end
@@ -114,6 +115,15 @@ module Gitlab
trans.add_tag(name, value) if trans
end
+ # Sets the action of the current transaction (if any)
+ #
+ # action - The name of the action.
+ def self.action=(action)
+ trans = current_transaction
+
+ trans.action = action if trans
+ end
+
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
if enabled?
diff --git a/lib/gitlab/push_data_builder.rb b/lib/gitlab/push_data_builder.rb
index 97d1edab9c1..67622f321a6 100644
--- a/lib/gitlab/push_data_builder.rb
+++ b/lib/gitlab/push_data_builder.rb
@@ -36,11 +36,12 @@ module Gitlab
commit.hook_attrs(with_changed_files: true)
end
- type = Gitlab::Git.tag_ref?(ref) ? "tag_push" : "push"
+ type = Gitlab::Git.tag_ref?(ref) ? 'tag_push' : 'push'
# Hash to be passed as post_receive_data
data = {
object_kind: type,
+ event_name: type,
before: oldrev,
after: newrev,
ref: ref,
diff --git a/lib/gitlab/url_builder.rb b/lib/gitlab/url_builder.rb
index f1943222edf..2bbbd3074e8 100644
--- a/lib/gitlab/url_builder.rb
+++ b/lib/gitlab/url_builder.rb
@@ -20,6 +20,8 @@ module Gitlab
merge_request_url(object)
when Note
note_url
+ when WikiPage
+ wiki_page_url
else
raise NotImplementedError.new("No URL builder defined for #{object.class}")
end
@@ -58,5 +60,9 @@ module Gitlab
project_snippet_url(snippet, anchor: dom_id(object))
end
end
+
+ def wiki_page_url
+ "#{Gitlab.config.gitlab.url}#{object.wiki.wiki_base_path}/#{object.slug}"
+ end
end
end
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 4a7ee7dbb64..247383aa46c 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -11,7 +11,7 @@ retry() {
return 1
}
-if [ -f /.dockerinit ]; then
+if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
mkdir -p vendor
# Install phantomjs package
diff --git a/spec/controllers/commit_controller_spec.rb b/spec/controllers/commit_controller_spec.rb
index f09e4fcb154..cf5c606c723 100644
--- a/spec/controllers/commit_controller_spec.rb
+++ b/spec/controllers/commit_controller_spec.rb
@@ -4,6 +4,8 @@ describe Projects::CommitController do
let(:project) { create(:project) }
let(:user) { create(:user) }
let(:commit) { project.commit("master") }
+ let(:master_pickable_sha) { '7d3b0f7cff5f37573aea97cebfd5692ea1689924' }
+ let(:master_pickable_commit) { project.commit(master_pickable_sha) }
before do
sign_in(user)
@@ -192,4 +194,53 @@ describe Projects::CommitController do
end
end
end
+
+ describe '#cherry_pick' do
+ context 'when target branch is not provided' do
+ it 'should render the 404 page' do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ id: master_pickable_commit.id)
+
+ expect(response).not_to be_success
+ expect(response.status).to eq(404)
+ end
+ end
+
+ context 'when the cherry-pick was successful' do
+ it 'should redirect to the commits page' do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+
+ expect(response).to redirect_to namespace_project_commits_path(project.namespace, project, 'master')
+ expect(flash[:notice]).to eq('The commit has been successfully cherry-picked.')
+ end
+ end
+
+ context 'when the cherry_pick failed' do
+ before do
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+ end
+
+ it 'should redirect to the commit page' do
+ # Cherry-picking a commit that has been already cherry-picked.
+ post(:cherry_pick,
+ namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ target_branch: 'master',
+ id: master_pickable_commit.id)
+
+ expect(response).to redirect_to namespace_project_commit_path(project.namespace, project, master_pickable_commit.id)
+ expect(flash[:alert]).to match('Sorry, we cannot cherry-pick this commit automatically.')
+ end
+ end
+ end
end
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
new file mode 100644
index 00000000000..40bd83af861
--- /dev/null
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+describe Projects::GroupLinksController do
+ let(:project) { create(:project, :private) }
+ let(:group) { create(:group, :private) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :master]
+ sign_in(user)
+ end
+
+ describe '#create' do
+ shared_context 'link project to group' do
+ before do
+ post(:create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ link_group_id: group.id,
+ link_group_access: ProjectGroupLink.default_access)
+ end
+ end
+
+ context 'when user has access to group he want to link project to' do
+ before { group.add_developer(user) }
+ include_context 'link project to group'
+
+ it 'links project with selected group' do
+ expect(group.shared_projects).to include project
+ end
+
+ it 'redirects to project group links page'do
+ expect(response).to redirect_to(
+ namespace_project_group_links_path(project.namespace, project)
+ )
+ end
+ end
+
+ context 'when user doers not have access to group he want to link to' do
+ include_context 'link project to group'
+
+ it 'renders 404' do
+ expect(response.status).to eq 404
+ end
+
+ it 'does not share project with that group' do
+ expect(group.shared_projects).to_not include project
+ end
+ end
+ end
+end
diff --git a/spec/factories/project_wikis.rb b/spec/factories/project_wikis.rb
new file mode 100644
index 00000000000..a3403fd76ae
--- /dev/null
+++ b/spec/factories/project_wikis.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :project_wiki do
+ project factory: :empty_project
+ user factory: :user
+ initialize_with { new(project, user) }
+ end
+end
diff --git a/spec/factories/wiki_pages.rb b/spec/factories/wiki_pages.rb
new file mode 100644
index 00000000000..938ccf2306b
--- /dev/null
+++ b/spec/factories/wiki_pages.rb
@@ -0,0 +1,9 @@
+require 'ostruct'
+
+FactoryGirl.define do
+ factory :wiki_page do
+ page = OpenStruct.new(url_path: 'some-name')
+ association :wiki, factory: :project_wiki, strategy: :build
+ initialize_with { new(wiki, page, true) }
+ end
+end
diff --git a/spec/features/builds_spec.rb b/spec/features/builds_spec.rb
index 6da3a857b3f..090a941958f 100644
--- a/spec/features/builds_spec.rb
+++ b/spec/features/builds_spec.rb
@@ -86,6 +86,20 @@ describe "Builds" do
end
end
end
+
+ context 'Build raw trace' do
+ before do
+ @build.run!
+ @build.trace = 'BUILD TRACE'
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it do
+ page.within('.build-controls') do
+ expect(page).to have_link 'Raw'
+ end
+ end
+ end
end
describe "POST /:project/builds/:id/cancel" do
@@ -120,4 +134,20 @@ describe "Builds" do
it { expect(page.response_headers['Content-Type']).to eq(artifacts_file.content_type) }
end
+
+ describe "GET /:project/builds/:id/raw" do
+ before do
+ Capybara.current_session.driver.header('X-Sendfile-Type', 'X-Sendfile')
+ @build.run!
+ @build.trace = 'BUILD TRACE'
+ visit namespace_project_build_path(@project.namespace, @project, @build)
+ end
+
+ it 'sends the right headers' do
+ page.within('.build-controls') { click_link 'Raw' }
+
+ expect(page.response_headers['Content-Type']).to eq('text/plain; charset=utf-8')
+ expect(page.response_headers['X-Sendfile']).to eq(@build.path_to_trace)
+ end
+ end
end
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index b189a97fbc0..7944403f874 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -122,9 +122,13 @@ feature 'Issue filtering by Labels', feature: true do
sleep 2
end
- it 'should not show "Bugfix1" or "Feature1" in issues list' do
+ it 'should show issue "Bugfix2" or "Feature1" in issues list' do
+ expect(page).to have_content "Bugfix2"
+ expect(page).to have_content "Feature1"
+ end
+
+ it 'should not show "Bugfix1" in issues list' do
expect(page).not_to have_content "Bugfix1"
- expect(page).not_to have_content "Feature1"
end
it 'should show label "enhancement" and "feature" in filtered-labels' do
@@ -137,7 +141,7 @@ feature 'Issue filtering by Labels', feature: true do
end
end
- context 'filter by label enhancement and bug in issues list', js: true do
+ context 'filter by label enhancement or bug in issues list', js: true do
before do
page.find('.js-label-select').click
sleep 0.5
@@ -147,8 +151,9 @@ feature 'Issue filtering by Labels', feature: true do
sleep 2
end
- it 'should show issue "Bugfix2" in issues list' do
+ it 'should show issue "Bugfix2" or "Bugfix1" in issues list' do
expect(page).to have_content "Bugfix2"
+ expect(page).to have_content "Bugfix1"
end
it 'should not show "Feature1"' do
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index 69b22232f10..192e3619375 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -84,14 +84,20 @@ describe 'Filter issues', feature: true do
it 'should filter by any label' do
find('.dropdown-menu-labels a', text: 'Any Label').click
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ sleep 2
+
page.within '.labels-filter' do
expect(page).to have_content 'Any Label'
end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Label')
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Any Label')
end
it 'should filter by no label' do
find('.dropdown-menu-labels a', text: 'No Label').click
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ sleep 2
+
page.within '.labels-filter' do
expect(page).to have_content 'No Label'
end
@@ -121,6 +127,7 @@ describe 'Filter issues', feature: true do
find('.js-label-select').click
find('.dropdown-menu-labels .dropdown-content a', text: label.title).click
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
sleep 2
end
diff --git a/spec/features/issues/update_issues_spec.rb b/spec/features/issues/update_issues_spec.rb
index 3eb903a93fe..b03dd0f666d 100644
--- a/spec/features/issues/update_issues_spec.rb
+++ b/spec/features/issues/update_issues_spec.rb
@@ -48,7 +48,7 @@ feature 'Multiple issue updating from issues#index', feature: true do
click_update_issues_button
page.within('.issue .controls') do
- expect(find('.author_link')["data-original-title"]).to have_content(user.name)
+ expect(find('.author_link')["title"]).to have_content(user.name)
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 35c8f93abc1..90476ab369b 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -112,7 +112,7 @@ describe 'Issues', feature: true do
end
describe 'filter issue' do
- titles = ['foo','bar','baz']
+ titles = %w[foo bar baz]
titles.each_with_index do |title, index|
let!(title.to_sym) do
create(:issue, title: title,
@@ -153,8 +153,94 @@ describe 'Issues', feature: true do
expect(first_issue).to include('baz')
end
+ describe 'sorting by due date' do
+ before do
+ foo.update(due_date: 1.day.from_now)
+ bar.update(due_date: 6.days.from_now)
+ end
+
+ it 'sorts by recently due date' do
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_soon)
+
+ expect(first_issue).to include('foo')
+ end
+
+ it 'sorts by least recently due date' do
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later)
+
+ expect(first_issue).to include('bar')
+ end
+
+ it 'sorts by least recently due date by excluding nil due dates' do
+ bar.update(due_date: nil)
+
+ visit namespace_project_issues_path(project.namespace, project, sort: sort_value_due_date_later)
+
+ expect(first_issue).to include('foo')
+ end
+ end
+
+ describe 'filtering by due date' do
+ before do
+ foo.update(due_date: 1.day.from_now)
+ bar.update(due_date: 6.days.from_now)
+ end
+
+ it 'filters by none' do
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::NoDueDate.name)
+
+ expect(page).not_to have_content('foo')
+ expect(page).not_to have_content('bar')
+ expect(page).to have_content('baz')
+ end
+
+ it 'filters by any' do
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::AnyDueDate.name)
+
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).to have_content('baz')
+ end
+
+ it 'filters by due this week' do
+ foo.update(due_date: Date.today.beginning_of_week + 2.days)
+ bar.update(due_date: Date.today.end_of_week)
+ baz.update(due_date: Date.today - 8.days)
+
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisWeek.name)
+
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).not_to have_content('baz')
+ end
+
+ it 'filters by due this month' do
+ foo.update(due_date: Date.today.beginning_of_month + 2.days)
+ bar.update(due_date: Date.today.end_of_month)
+ baz.update(due_date: Date.today - 50.days)
+
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::DueThisMonth.name)
+
+ expect(page).to have_content('foo')
+ expect(page).to have_content('bar')
+ expect(page).not_to have_content('baz')
+ end
+
+ it 'filters by overdue' do
+ foo.update(due_date: Date.today + 2.days)
+ bar.update(due_date: Date.today + 20.days)
+ baz.update(due_date: Date.yesterday)
+
+ visit namespace_project_issues_path(project.namespace, project, due_date: Issue::Overdue.name)
+
+ expect(page).not_to have_content('foo')
+ expect(page).not_to have_content('bar')
+ expect(page).to have_content('baz')
+ end
+ end
+
describe 'sorting by milestone' do
- before :each do
+ before do
foo.milestone = newer_due_milestone
foo.save
bar.milestone = later_due_milestone
@@ -177,7 +263,7 @@ describe 'Issues', feature: true do
describe 'combine filter and sort' do
let(:user2) { create(:user) }
- before :each do
+ before do
foo.assignee = user2
foo.save
bar.assignee = user2
@@ -224,7 +310,7 @@ describe 'Issues', feature: true do
let(:guest) { create(:user) }
- before :each do
+ before do
project.team << [[guest], :guest]
end
@@ -267,7 +353,7 @@ describe 'Issues', feature: true do
context 'by unauthorized user' do
let(:guest) { create(:user) }
- before :each do
+ before do
project.team << [guest, :guest]
issue.milestone = milestone
issue.save
@@ -285,7 +371,7 @@ describe 'Issues', feature: true do
describe 'removing assignee' do
let(:user2) { create(:user) }
- before :each do
+ before do
issue.assignee = user2
issue.save
end
diff --git a/spec/features/merge_requests/cherry_pick_spec.rb b/spec/features/merge_requests/cherry_pick_spec.rb
new file mode 100644
index 00000000000..82bc5226d07
--- /dev/null
+++ b/spec/features/merge_requests/cherry_pick_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Cherry-pick Merge Requests' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:merge_request) { create(:merge_request_with_diffs, source_project: project, author: user) }
+
+ before do
+ login_as user
+ project.team << [user, :master]
+ end
+
+ context "Viewing a merged merge request" do
+ before do
+ service = MergeRequests::MergeService.new(project, user)
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ end
+ end
+
+ # Fast-forward merge, or merged before GitLab 8.5.
+ context "Without a merge commit" do
+ before do
+ merge_request.merge_commit_sha = nil
+ merge_request.save
+ end
+
+ it "doesn't show a Cherry-pick button" do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).not_to have_link "Cherry-pick"
+ end
+ end
+
+ context "With a merge commit" do
+ it "shows a Cherry-pick button" do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+
+ expect(page).to have_link "Cherry-pick"
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index c57ab5f3b03..e3ecd60a5f3 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -2,8 +2,14 @@ require 'rails_helper'
feature 'Merge Request filtering by Milestone', feature: true do
let(:project) { create(:project, :public) }
+ let!(:user) { create(:user)}
let(:milestone) { create(:milestone, project: project) }
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
scenario 'filters by no Milestone', js: true do
create(:merge_request, :with_diffs, source_project: project)
create(:merge_request, :simple, source_project: project, milestone: milestone)
diff --git a/spec/features/project/commits/cherry_pick_spec.rb b/spec/features/project/commits/cherry_pick_spec.rb
new file mode 100644
index 00000000000..0559b02f321
--- /dev/null
+++ b/spec/features/project/commits/cherry_pick_spec.rb
@@ -0,0 +1,67 @@
+require 'spec_helper'
+
+describe 'Cherry-pick Commits' do
+ let(:project) { create(:project) }
+ let(:master_pickable_commit) { project.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:master_pickable_merge) { project.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+
+
+ before do
+ login_as :user
+ project.team << [@user, :master]
+ visit namespace_project_commits_path(project.namespace, project, project.repository.root_ref, { limit: 5 })
+ 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
+ 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
+
+ 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'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('The commit has been successfully cherry-picked.')
+ end
+ end
+
+ 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'
+ click_button 'Cherry-pick'
+ end
+ 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'
+ click_button 'Cherry-pick'
+ end
+ expect(page).to have_content('Sorry, we cannot cherry-pick this commit automatically.')
+ end
+ end
+
+ 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'
+ end
+ 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
+end
diff --git a/spec/features/projects/files/project_owner_creates_license_file_spec.rb b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
new file mode 100644
index 00000000000..3d6ffbc4c6b
--- /dev/null
+++ b/spec/features/projects/files/project_owner_creates_license_file_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+feature 'project owner creates a license file', feature: true, js: true do
+ include Select2Helper
+
+ let(:project_master) { create(:user) }
+ let(:project) { create(:project) }
+ background do
+ project.repository.remove_file(project_master, 'LICENSE', 'Remove LICENSE', 'master')
+ project.team << [project_master, :master]
+ login_as(project_master)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'project master creates a license file manually from a template' do
+ visit namespace_project_tree_path(project.namespace, project, project.repository.root_ref)
+ find('.add-to-tree').click
+ click_link 'New file'
+
+ fill_in :file_name, with: 'LICENSE'
+
+ expect(page).to have_selector('.license-selector')
+
+ select2('mit', from: '#license_type')
+
+ file_content = find('.file-content')
+ expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+
+ fill_in :commit_message, with: 'Add a LICENSE file', visible: true
+ click_button 'Commit Changes'
+
+ expect(current_path).to eq(
+ namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
+ expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ end
+
+ scenario 'project master creates a license file from the "Add license" link' do
+ click_link 'Add License'
+
+ expect(current_path).to eq(
+ namespace_project_new_blob_path(project.namespace, project, 'master'))
+ expect(find('#file_name').value).to eq('LICENSE')
+ expect(page).to have_selector('.license-selector')
+
+ select2('mit', from: '#license_type')
+
+ file_content = find('.file-content')
+ expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+
+ fill_in :commit_message, with: 'Add a LICENSE file', visible: true
+ click_button 'Commit Changes'
+
+ expect(current_path).to eq(
+ namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
+ expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ end
+end
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
new file mode 100644
index 00000000000..3268e240200
--- /dev/null
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+feature 'project owner sees a link to create a license file in empty project', feature: true, js: true do
+ include Select2Helper
+
+ let(:project_master) { create(:user) }
+ let(:project) { create(:empty_project) }
+ background do
+ project.team << [project_master, :master]
+ login_as(project_master)
+ end
+
+ scenario 'project master creates a license file from a template' do
+ visit namespace_project_path(project.namespace, project)
+ click_link 'Create empty bare repository'
+ click_on 'LICENSE'
+
+ expect(current_path).to eq(
+ namespace_project_new_blob_path(project.namespace, project, 'master'))
+ expect(find('#file_name').value).to eq('LICENSE')
+ expect(page).to have_selector('.license-selector')
+
+ select2('mit', from: '#license_type')
+
+ file_content = find('.file-content')
+ expect(file_content).to have_content('The MIT License (MIT)')
+ expect(file_content).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+
+ fill_in :commit_message, with: 'Add a LICENSE file', visible: true
+ # Remove pre-receive hook so we can push without auth
+ FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
+ click_button 'Commit Changes'
+
+ expect(current_path).to eq(
+ namespace_project_blob_path(project.namespace, project, 'master/LICENSE'))
+ expect(page).to have_content('The MIT License (MIT)')
+ expect(page).to have_content("Copyright (c) 2016 #{project.namespace.human_name}")
+ end
+end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index e8886e7edf9..8edeb8d18af 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -80,6 +80,22 @@ describe "Runners" do
end
end
+ describe "shared runners description" do
+ let(:shared_runners_text) { 'custom **shared** runners description' }
+ let(:shared_runners_html) { 'custom shared runners description' }
+
+ before do
+ stub_application_setting(shared_runners_text: shared_runners_text)
+ project = FactoryGirl.create :empty_project, shared_runners_enabled: false
+ project.team << [user, :master]
+ visit runners_path(project)
+ end
+
+ it "sees shared runners description" do
+ expect(page.find(".shared-runners-description")).to have_content(shared_runners_html)
+ end
+ end
+
describe "show page" do
before do
@project = FactoryGirl.create :empty_project
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
new file mode 100644
index 00000000000..113d4c40cfc
--- /dev/null
+++ b/spec/features/todos/todos_spec.rb
@@ -0,0 +1,79 @@
+require 'spec_helper'
+
+describe 'Dashboard Todos', feature: true do
+ let(:user){ create(:user) }
+ let(:author){ create(:user) }
+ let(:project){ create(:project) }
+ let(:issue){ create(:issue) }
+ let(:todos_per_page){ Todo.default_per_page }
+ let(:todos_total){ todos_per_page + 1 }
+
+ describe 'GET /dashboard/todos' do
+ context 'User does not have todos' do
+ before do
+ login_as(user)
+ visit dashboard_todos_path
+ end
+ it 'shows "All done" message' do
+ expect(page).to have_content "You're all done!"
+ end
+ end
+
+ context 'User has a todo', js: true do
+ before do
+ create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'todo is present' do
+ expect(page).to have_selector('.todos-list .todo', count: 1)
+ end
+
+ describe 'deleting the todo' do
+ before do
+ first('.done-todo').click
+ end
+
+ it 'is removed from the list' do
+ expect(page).not_to have_selector('.todos-list .todo')
+ end
+
+ it 'shows "All done" message' do
+ expect(page).to have_content("You're all done!")
+ end
+ end
+ end
+
+ context 'User has multiple pages of Todos' do
+ let(:todo_total_pages){ (todos_total.to_f/todos_per_page).ceil }
+
+ before do
+ todos_total.times do
+ create(:todo, :mentioned, user: user, project: project, target: issue, author: author)
+ end
+
+ login_as(user)
+ visit dashboard_todos_path
+ end
+
+ it 'is paginated' do
+ expect(page).to have_selector('.gl-pagination')
+ end
+
+ it 'is has the right number of pages' do
+ expect(page).to have_selector('.gl-pagination .page', count: todo_total_pages)
+ end
+
+ describe 'deleting last todo from last page', js: true do
+ it 'redirects to the previous page' do
+ page.within('.gl-pagination') do
+ click_link todo_total_pages.to_s
+ end
+ first('.done-todo').click
+ expect(page).to have_content(Todo.last.body)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index b1648055462..bc607a29751 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -62,6 +62,22 @@ describe IssuesFinder do
expect(issues).to eq([issue2])
end
+ it 'returns unique issues when filtering by multiple labels' do
+ label2 = create(:label, project: project2)
+
+ create(:label_link, label: label2, target: issue2)
+
+ params = {
+ scope: 'all',
+ label_name: [label.title, label2.title].join(','),
+ state: 'opened'
+ }
+
+ issues = IssuesFinder.new(user, params).execute
+
+ expect(issues).to eq([issue2])
+ end
+
it 'should filter by no label name' do
params = { scope: "all", label_name: Label::None.title, state: 'opened' }
issues = IssuesFinder.new(user, params).execute
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
new file mode 100644
index 00000000000..727c25ff529
--- /dev/null
+++ b/spec/helpers/commits_helper_spec.rb
@@ -0,0 +1,29 @@
+require 'rails_helper'
+
+describe CommitsHelper do
+ describe 'commit_author_link' do
+ it 'escapes the author email' do
+ commit = double(
+ author: nil,
+ author_name: 'Persistent XSS',
+ author_email: 'my@email.com" onmouseover="alert(1)'
+ )
+
+ expect(helper.commit_author_link(commit)).
+ not_to include('onmouseover="alert(1)"')
+ end
+ end
+
+ describe 'commit_committer_link' do
+ it 'escapes the committer email' do
+ commit = double(
+ committer: nil,
+ committer_name: 'Persistent XSS',
+ committer_email: 'my@email.com" onmouseover="alert(1)'
+ )
+
+ expect(helper.commit_committer_link(commit)).
+ not_to include('onmouseover="alert(1)"')
+ end
+ end
+end
diff --git a/spec/helpers/import_helper_spec.rb b/spec/helpers/import_helper_spec.rb
new file mode 100644
index 00000000000..3391234e9f5
--- /dev/null
+++ b/spec/helpers/import_helper_spec.rb
@@ -0,0 +1,25 @@
+require 'rails_helper'
+
+describe ImportHelper do
+ describe '#github_project_link' do
+ context 'when provider does not specify a custom URL' do
+ it 'uses default GitHub URL' do
+ allow(Gitlab.config.omniauth).to receive(:providers).
+ and_return([Settingslogic.new('name' => 'github')])
+
+ expect(helper.github_project_link('octocat/Hello-World')).
+ to include('href="https://github.com/octocat/Hello-World"')
+ end
+ end
+
+ context 'when provider specify a custom URL' do
+ it 'uses custom URL' do
+ allow(Gitlab.config.omniauth).to receive(:providers).
+ and_return([Settingslogic.new('name' => 'github', 'url' => 'https://github.company.com')])
+
+ expect(helper.github_project_link('octocat/Hello-World')).
+ to include('href="https://github.company.com/octocat/Hello-World"')
+ end
+ end
+ end
+end
diff --git a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
index 5f4b63bcafb..643acf0303c 100644
--- a/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
+++ b/spec/lib/ci/gitlab_ci_yaml_processor_spec.rb
@@ -286,6 +286,81 @@ module Ci
end
end
+
+ describe "Scripts handling" do
+ let(:config_data) { YAML.dump(config) }
+ let(:config_processor) { GitlabCiYamlProcessor.new(config_data, path) }
+
+ subject { config_processor.builds_for_stage_and_ref("test", "master").first }
+
+ describe "before_script" do
+ context "in global context" do
+ let(:config) do
+ {
+ before_script: ["global script"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("global script\nscript")
+ end
+ end
+
+ context "overwritten in local context" do
+ let(:config) do
+ {
+ before_script: ["global script"],
+ test: { before_script: ["local script"], script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("local script\nscript")
+ end
+ end
+ end
+
+ describe "script" do
+ let(:config) do
+ {
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return commands with scripts concencaced" do
+ expect(subject[:commands]).to eq("script")
+ end
+ end
+
+ describe "after_script" do
+ context "in global context" do
+ let(:config) do
+ {
+ after_script: ["after_script"],
+ test: { script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["after_script"])
+ end
+ end
+
+ context "overwritten in local context" do
+ let(:config) do
+ {
+ after_script: ["local after_script"],
+ test: { after_script: ["local after_script"], script: ["script"] }
+ }
+ end
+
+ it "return after_script in options" do
+ expect(subject[:options][:after_script]).to eq(["local after_script"])
+ end
+ end
+ end
+ end
describe "Image and service handling" do
it "returns image and service when defined" do
@@ -592,7 +667,7 @@ module Ci
stage_idx: 1,
name: :normal_job,
only: nil,
- commands: "\ntest",
+ commands: "test",
tag_list: [],
options: {},
when: "on_success",
@@ -619,7 +694,7 @@ EOT
stage_idx: 1,
name: :job1,
only: nil,
- commands: "\nexecute-script-for-job",
+ commands: "execute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
@@ -631,7 +706,7 @@ EOT
stage_idx: 1,
name: :job2,
only: nil,
- commands: "\nexecute-script-for-job",
+ commands: "execute-script-for-job",
tag_list: [],
options: {},
when: "on_success",
@@ -663,6 +738,27 @@ EOT
end.to raise_error(GitlabCiYamlProcessor::ValidationError, "before_script should be an array of strings")
end
+ it "returns errors if job before_script parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", before_script: [10, "test"] } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: before_script should be an array of strings")
+ end
+
+ it "returns errors if after_script parameter is invalid" do
+ config = YAML.dump({ after_script: "bundle update", rspec: { script: "test" } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "after_script should be an array of strings")
+ end
+
+ it "returns errors if job after_script parameter is not an array of strings" do
+ config = YAML.dump({ rspec: { script: "test", after_script: [10, "test"] } })
+ expect do
+ GitlabCiYamlProcessor.new(config, path)
+ end.to raise_error(GitlabCiYamlProcessor::ValidationError, "rspec job: after_script should be an array of strings")
+ end
+
it "returns errors if image parameter is invalid" do
config = YAML.dump({ image: ["test"], rspec: { script: "test" } })
expect do
diff --git a/spec/lib/gitlab/metrics_spec.rb b/spec/lib/gitlab/metrics_spec.rb
index 10177c0e8dd..96f7eabbca6 100644
--- a/spec/lib/gitlab/metrics_spec.rb
+++ b/spec/lib/gitlab/metrics_spec.rb
@@ -123,4 +123,28 @@ describe Gitlab::Metrics do
end
end
end
+
+ describe '.action=' do
+ context 'without a transaction' do
+ it 'does nothing' do
+ expect_any_instance_of(Gitlab::Metrics::Transaction).
+ not_to receive(:action=)
+
+ Gitlab::Metrics.action = 'foo'
+ end
+ end
+
+ context 'with a transaction' do
+ it 'sets the action of a transaction' do
+ trans = Gitlab::Metrics::Transaction.new
+
+ expect(Gitlab::Metrics).to receive(:current_transaction).
+ and_return(trans)
+
+ expect(trans).to receive(:action=).with('foo')
+
+ Gitlab::Metrics.action = 'foo'
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/push_data_builder_spec.rb b/spec/lib/gitlab/push_data_builder_spec.rb
index 961022b9d12..7fc34139eff 100644
--- a/spec/lib/gitlab/push_data_builder_spec.rb
+++ b/spec/lib/gitlab/push_data_builder_spec.rb
@@ -14,11 +14,11 @@ describe Gitlab::PushDataBuilder, lib: true do
it { expect(data[:ref]).to eq('refs/heads/master') }
it { expect(data[:commits].size).to eq(3) }
it { expect(data[:total_commits_count]).to eq(3) }
- it { expect(data[:commits].first[:added]).to eq(["gitlab-grack"]) }
- it { expect(data[:commits].first[:modified]).to eq([".gitmodules"]) }
+ it { expect(data[:commits].first[:added]).to eq(['gitlab-grack']) }
+ it { expect(data[:commits].first[:modified]).to eq(['.gitmodules']) }
it { expect(data[:commits].first[:removed]).to eq([]) }
- include_examples 'project hook data'
+ include_examples 'project hook data with deprecateds'
include_examples 'deprecated repository hook data'
end
@@ -34,9 +34,18 @@ describe Gitlab::PushDataBuilder, lib: true do
it { expect(data[:checkout_sha]).to eq('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
it { expect(data[:after]).to eq('8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b') }
it { expect(data[:ref]).to eq('refs/tags/v1.1.0') }
+ it { expect(data[:user_id]).to eq(user.id) }
+ it { expect(data[:user_name]).to eq(user.name) }
+ it { expect(data[:user_email]).to eq(user.email) }
+ it { expect(data[:user_avatar]).to eq(user.avatar_url) }
+ it { expect(data[:project_id]).to eq(project.id) }
+ it { expect(data[:project]).to be_a(Hash) }
it { expect(data[:commits]).to be_empty }
it { expect(data[:total_commits_count]).to be_zero }
+ include_examples 'project hook data with deprecateds'
+ include_examples 'deprecated repository hook data'
+
it 'does not raise an error when given nil commits' do
expect { described_class.build(spy, spy, spy, spy, spy, nil) }.
not_to raise_error
diff --git a/spec/lib/gitlab/url_builder_spec.rb b/spec/lib/gitlab/url_builder_spec.rb
index 6ffc0d6e658..bf11472407a 100644
--- a/spec/lib/gitlab/url_builder_spec.rb
+++ b/spec/lib/gitlab/url_builder_spec.rb
@@ -106,5 +106,14 @@ describe Gitlab::UrlBuilder, lib: true do
end
end
end
+
+ context 'when passing a WikiPage' do
+ it 'returns a proper URL' do
+ wiki_page = build(:wiki_page)
+ url = described_class.build(wiki_page)
+
+ expect(url).to eq "#{Gitlab.config.gitlab.url}#{wiki_page.wiki.wiki_base_path}/#{wiki_page.slug}"
+ end
+ end
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 631b5094f42..495c5cbac00 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -213,7 +213,7 @@ describe Notify do
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains a link to the new merge request' do
@@ -268,7 +268,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the name of the previous assignee' do
@@ -302,7 +302,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the names of the added labels' do
@@ -331,7 +331,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/i
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/i
end
it 'contains the new status' do
@@ -364,7 +364,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains the new status' do
@@ -502,7 +502,7 @@ describe Notify do
it_behaves_like 'an unsubscribeable thread'
it 'has the correct subject' do
- is_expected.to have_subject /#{merge_request.title} \(##{merge_request.iid}\)/
+ is_expected.to have_subject /#{merge_request.title} \(#{merge_request.to_reference}\)/
end
it 'contains a link to the merge request note' do
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index fac516f9568..060e6599104 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -191,18 +191,36 @@ describe Issue, models: true do
end
describe '#related_branches' do
- it 'selects the right branches' do
+ let(:user) { build(:admin) }
+
+ before do
allow(subject.project.repository).to receive(:branch_names).
- and_return(['mpempe', "#{subject.iid}mepmep", subject.to_branch_name])
+ and_return(["mpempe", "#{subject.iid}mepmep", subject.to_branch_name, "#{subject.iid}-branch"])
+
+ # Without this stub, the `create(:merge_request)` above fails because it can't find
+ # the source branch. This seems like a reasonable compromise, in comparison with
+ # setting up a full repo here.
+ allow_any_instance_of(MergeRequest).to receive(:create_merge_request_diff)
+ end
+
+ it "selects the right branches when there are no referenced merge requests" do
+ expect(subject.related_branches(user)).to eq([subject.to_branch_name, "#{subject.iid}-branch"])
+ end
- expect(subject.related_branches).to eq([subject.to_branch_name])
+ it "selects the right branches when there is a referenced merge request" do
+ merge_request = create(:merge_request, { description: "Closes ##{subject.iid}",
+ source_project: subject.project,
+ source_branch: "#{subject.iid}-branch" })
+ merge_request.create_cross_references!(user)
+ expect(subject.referenced_merge_requests).to_not be_empty
+ expect(subject.related_branches(user)).to eq([subject.to_branch_name])
end
it 'excludes stable branches from the related branches' do
allow(subject.project.repository).to receive(:branch_names).
and_return(["#{subject.iid}-0-stable"])
- expect(subject.related_branches).to eq []
+ expect(subject.related_branches(user)).to eq []
end
end
@@ -217,11 +235,20 @@ describe Issue, models: true do
let(:subject) { create :issue }
end
- describe '#to_branch_name' do
- let(:issue) { create(:issue, title: 'a' * 30) }
+ describe "#to_branch_name" do
+ let(:issue) { create(:issue, title: 'testing-issue') }
it 'starts with the issue iid' do
- expect(issue.to_branch_name).to match /\A#{issue.iid}-a+\z/
+ expect(issue.to_branch_name).to match /\A#{issue.iid}-[A-Za-z\-]+\z/
+ end
+
+ it "contains the issue title if not confidential" do
+ expect(issue.to_branch_name).to match /testing-issue\z/
+ end
+
+ it "does not contain the issue title if confidential" do
+ issue = create(:issue, title: 'testing-issue', confidential: true)
+ expect(issue.to_branch_name).to match /confidential-issue\z/
end
end
end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 91dd92b7c67..d878162a220 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -152,7 +152,7 @@ describe HipchatService, models: true do
obj_attr = merge_sample_data[:object_attributes]
expect(message).to eq("#{user.name} opened " \
- "<a href=\"#{obj_attr[:url]}\">merge request ##{obj_attr["iid"]}</a> in " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{obj_attr["iid"]}</a> in " \
"<a href=\"#{project.web_url}\">#{project_name}</a>: " \
"<b>Awesome merge request</b>" \
"<pre>please fix</pre>")
@@ -202,7 +202,7 @@ describe HipchatService, models: true do
title = data[:merge_request]['title']
expect(message).to eq("#{user.name} commented on " \
- "<a href=\"#{obj_attr[:url]}\">merge request ##{merge_id}</a> in " \
+ "<a href=\"#{obj_attr[:url]}\">merge request !#{merge_id}</a> in " \
"<a href=\"#{project.web_url}\">#{project_name}</a>: " \
"<b>#{title}</b>" \
"<pre>merge request note</pre>")
diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb
index dae8bd90922..224c7ceabe8 100644
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ b/spec/models/project_services/slack_service/merge_message_spec.rb
@@ -31,7 +31,7 @@ describe SlackService::MergeMessage, models: true do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'Test User opened <somewhere.com/merge_requests/100|merge request #100> '\
+ 'Test User opened <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
@@ -43,7 +43,7 @@ describe SlackService::MergeMessage, models: true do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'Test User closed <somewhere.com/merge_requests/100|merge request #100> '\
+ 'Test User closed <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 06006b9a4f5..d37590cab75 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a merge request' do
message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq("Test User commented on " \
- "<url|merge request #30> in <somewhere.com|project_name>: " \
+ "<url|merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*")
expected_attachments = [
{
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
new file mode 100644
index 00000000000..6ecab645b49
--- /dev/null
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe SlackService::WikiPageMessage, models: true do
+ subject { described_class.new(args) }
+
+ let(:args) do
+ {
+ user: {
+ name: 'Test User',
+ username: 'Test User'
+ },
+ project_name: 'project_name',
+ project_url: 'somewhere.com',
+ object_attributes: {
+ title: 'Wiki page title',
+ url: 'url',
+ content: 'Wiki page description'
+ }
+ }
+ end
+
+ describe '#pretext' do
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+ it 'returns a message that a new wiki page was created' do
+ expect(subject.pretext).to eq(
+ 'Test User created <url|wiki page> in <somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
+ end
+
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
+
+ it 'returns a message that a wiki page was updated' do
+ expect(subject.pretext).to eq(
+ 'Test User edited <url|wiki page> in <somewhere.com|project_name>: '\
+ '*Wiki page title*')
+ end
+ end
+ end
+
+ describe '#attachments' do
+ let(:color) { '#345' }
+
+ context 'when :action == "create"' do
+ before { args[:object_attributes][:action] = 'create' }
+
+
+ it 'it returns the attachment for a new wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page description",
+ color: color,
+ }
+ ])
+ end
+ end
+
+ context 'when :action == "update"' do
+ before { args[:object_attributes][:action] = 'update' }
+
+ it 'it returns the attachment for an updated wiki page' do
+ expect(subject.attachments).to eq([
+ {
+ text: "Wiki page description",
+ color: color,
+ }
+ ])
+ end
+ end
+ end
+end
diff --git a/spec/models/project_services/slack_service_spec.rb b/spec/models/project_services/slack_service_spec.rb
index a9e0afad90f..478d59be08b 100644
--- a/spec/models/project_services/slack_service_spec.rb
+++ b/spec/models/project_services/slack_service_spec.rb
@@ -75,6 +75,17 @@ describe SlackService, models: true do
@merge_request = merge_service.execute
@merge_sample_data = merge_service.hook_data(@merge_request,
'open')
+
+ opts = {
+ title: "Awesome wiki_page",
+ content: "Some text describing some thing or another",
+ format: "md",
+ message: "user created page: Awesome wiki_page"
+ }
+
+ wiki_page_service = WikiPages::CreateService.new(project, user, opts)
+ @wiki_page = wiki_page_service.execute
+ @wiki_page_sample_data = wiki_page_service.hook_data(@wiki_page, 'create')
end
it "should call Slack API for push events" do
@@ -95,6 +106,12 @@ describe SlackService, models: true do
expect(WebMock).to have_requested(:post, webhook_url).once
end
+ it "should call Slack API for wiki page events" do
+ slack.execute(@wiki_page_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
+
it 'should use the username as an option for slack when configured' do
allow(slack).to receive(:username).and_return(username)
expect(Slack::Notifier).to receive(:new).
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f30a21e79ae..b561aa663d1 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -135,22 +135,69 @@ describe Repository, models: true do
end
- describe "#license" do
+ describe '#license_blob' do
before do
- repository.send(:cache).expire(:license)
+ repository.send(:cache).expire(:license_blob)
+ repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
end
- it 'test selection preference' do
- files = [TestBlob.new('file'), TestBlob.new('license'), TestBlob.new('copying')]
- expect(repository.tree).to receive(:blobs).and_return(files)
+ it 'looks in the root_ref only' do
+ repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'markdown')
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'markdown', false)
+
+ expect(repository.license_blob).to be_nil
+ end
- expect(repository.license.name).to eq('license')
+ it 'favors license file with no extension' do
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENSE.md', Licensee::License.new('mit').content, 'Add LICENSE.md', 'master', false)
+
+ expect(repository.license_blob.name).to eq('LICENSE')
end
- it 'also accepts licence instead of license' do
- expect(repository.tree).to receive(:blobs).and_return([TestBlob.new('licence')])
+ it 'favors .md file to .txt' do
+ repository.commit_file(user, 'LICENSE.md', Licensee::License.new('mit').content, 'Add LICENSE.md', 'master', false)
+ repository.commit_file(user, 'LICENSE.txt', Licensee::License.new('mit').content, 'Add LICENSE.txt', 'master', false)
- expect(repository.license.name).to eq('licence')
+ expect(repository.license_blob.name).to eq('LICENSE.md')
+ end
+
+ it 'favors LICENCE to LICENSE' do
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'LICENCE', Licensee::License.new('mit').content, 'Add LICENCE', 'master', false)
+
+ expect(repository.license_blob.name).to eq('LICENCE')
+ end
+
+ it 'favors LICENSE to COPYING' do
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+ repository.commit_file(user, 'COPYING', Licensee::License.new('mit').content, 'Add COPYING', 'master', false)
+
+ expect(repository.license_blob.name).to eq('LICENSE')
+ end
+
+ it 'favors LICENCE to COPYING' do
+ repository.commit_file(user, 'LICENCE', Licensee::License.new('mit').content, 'Add LICENCE', 'master', false)
+ repository.commit_file(user, 'COPYING', Licensee::License.new('mit').content, 'Add COPYING', 'master', false)
+
+ expect(repository.license_blob.name).to eq('LICENCE')
+ end
+ end
+
+ describe '#license_key' do
+ before do
+ repository.send(:cache).expire(:license_key)
+ repository.remove_file(user, 'LICENSE', 'Remove LICENSE', 'master')
+ end
+
+ it 'returns "no-license" when no license is detected' do
+ expect(repository.license_key).to eq('no-license')
+ end
+
+ it 'returns the license key' do
+ repository.commit_file(user, 'LICENSE', Licensee::License.new('mit').content, 'Add LICENSE', 'master', false)
+
+ expect(repository.license_key).to eq('mit')
end
end
@@ -541,6 +588,41 @@ describe Repository, models: true do
end
end
+ describe '#cherry_pick' do
+ let(:conflict_commit) { repository.commit('c642fe9b8b9f28f9225d7ea953fe14e74748d53b') }
+ let(:pickable_commit) { repository.commit('7d3b0f7cff5f37573aea97cebfd5692ea1689924') }
+ let(:pickable_merge) { repository.commit('e56497bb5f03a90a51293fc6d516788730953899') }
+
+ context 'when there is a conflict' do
+ it 'should abort the operation' do
+ expect(repository.cherry_pick(user, conflict_commit, 'master')).to eq(false)
+ end
+ end
+
+ context 'when commit was already cherry-picked' do
+ it 'should abort the operation' do
+ repository.cherry_pick(user, pickable_commit, 'master')
+
+ expect(repository.cherry_pick(user, pickable_commit, 'master')).to eq(false)
+ end
+ end
+
+ context 'when commit can be cherry-picked' do
+ it 'should cherry-pick the changes' do
+ expect(repository.cherry_pick(user, pickable_commit, 'master')).to be_truthy
+ end
+ end
+
+ context 'cherry-picking a merge commit' do
+ it 'should cherry-pick the changes' do
+ expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).to be_nil
+
+ repository.cherry_pick(user, pickable_merge, 'master')
+ expect(repository.blob_at_branch('master', 'foo/bar/.gitkeep')).not_to be_nil
+ end
+ end
+ end
+
describe '#before_delete' do
describe 'when a repository does not exist' do
before do
diff --git a/spec/requests/api/licenses_spec.rb b/spec/requests/api/licenses_spec.rb
new file mode 100644
index 00000000000..c17dcb222a9
--- /dev/null
+++ b/spec/requests/api/licenses_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+
+describe API::Licenses, api: true do
+ include ApiHelpers
+
+ describe 'Entity' do
+ before { get api('/licenses/mit') }
+
+ it { expect(json_response['key']).to eq('mit') }
+ it { expect(json_response['name']).to eq('MIT License') }
+ it { expect(json_response['nickname']).to be_nil }
+ it { expect(json_response['popular']).to be true }
+ it { expect(json_response['html_url']).to eq('http://choosealicense.com/licenses/mit/') }
+ it { expect(json_response['source_url']).to eq('https://opensource.org/licenses/MIT') }
+ it { expect(json_response['description']).to include('A permissive license that is short and to the point.') }
+ it { expect(json_response['conditions']).to eq(%w[include-copyright]) }
+ it { expect(json_response['permissions']).to eq(%w[commercial-use modifications distribution private-use]) }
+ it { expect(json_response['limitations']).to eq(%w[no-liability]) }
+ it { expect(json_response['content']).to include('The MIT License (MIT)') }
+ end
+
+ describe 'GET /licenses' do
+ it 'returns a list of available license templates' do
+ get api('/licenses')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(15)
+ expect(json_response.map { |l| l['key'] }).to include('agpl-3.0')
+ end
+
+ describe 'the popular parameter' do
+ context 'with popular=1' do
+ it 'returns a list of available popular license templates' do
+ get api('/licenses?popular=1')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(3)
+ expect(json_response.map { |l| l['key'] }).to include('apache-2.0')
+ end
+ end
+ end
+ end
+
+ describe 'GET /licenses/:key' do
+ context 'with :project and :fullname given' do
+ before do
+ get api("/licenses/#{license_type}?project=My+Awesome+Project&fullname=Anton+#{license_type.upcase}")
+ end
+
+ context 'for the mit license' do
+ let(:license_type) { 'mit' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('The MIT License (MIT)')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('Copyright (c) 2016 Anton')
+ end
+ end
+
+ context 'for the agpl-3.0 license' do
+ let(:license_type) { 'agpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU AFFERO GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ end
+ end
+
+ context 'for the gpl-3.0 license' do
+ let(:license_type) { 'gpl-3.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ end
+ end
+
+ context 'for the gpl-2.0 license' do
+ let(:license_type) { 'gpl-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('GNU GENERAL PUBLIC LICENSE')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('My Awesome Project')
+ expect(json_response['content']).to include('Copyright (C) 2016 Anton')
+ end
+ end
+
+ context 'for the apache-2.0 license' do
+ let(:license_type) { 'apache-2.0' }
+
+ it 'returns the license text' do
+ expect(json_response['content']).to include('Apache License')
+ end
+
+ it 'replaces placeholder values' do
+ expect(json_response['content']).to include('Copyright 2016 Anton')
+ end
+ end
+
+ context 'for an uknown license' do
+ let(:license_type) { 'muth-over9000' }
+
+ it 'returns a 404' do
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ context 'with no :fullname given' do
+ context 'with an authenticated user' do
+ let(:user) { create(:user) }
+
+ it 'replaces the copyright owner placeholder with the name of the current user' do
+ get api('/licenses/mit', user)
+
+ expect(json_response['content']).to include("Copyright (c) 2016 #{user.name}")
+ end
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/tags_spec.rb b/spec/requests/api/tags_spec.rb
index 9f9c3b1cf4c..edcb2bedbf7 100644
--- a/spec/requests/api/tags_spec.rb
+++ b/spec/requests/api/tags_spec.rb
@@ -32,9 +32,11 @@ describe API::API, api: true do
it "should return an array of project tags with release info" do
get api("/projects/#{project.id}/repository/tags", user)
+
expect(response.status).to eq(200)
expect(json_response).to be_an Array
expect(json_response.first['name']).to eq(tag_name)
+ expect(json_response.first['message']).to eq('Version 1.1.0')
expect(json_response.first['release']['description']).to eq(description)
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index 57d7eb927fd..ebd16c7efbe 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -156,6 +156,52 @@ describe Ci::API::API do
end
end
+ describe 'PATCH /builds/:id/trace.txt' do
+ let(:build) { create(:ci_build, :trace, runner_id: runner.id) }
+ let(:headers) { { Ci::API::Helpers::BUILD_TOKEN_HEADER => build.token, 'Content-Type' => 'text/plain' } }
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '11-20' }) }
+
+ before do
+ build.run!
+ patch ci_api("/builds/#{build.id}/trace.txt"), ' appended', headers_with_range
+ end
+
+ context 'when request is valid' do
+ it { expect(response.status).to eq 202 }
+ it { expect(build.reload.trace).to eq 'BUILD TRACE appended' }
+ it { expect(response.header).to have_key 'Range' }
+ it { expect(response.header).to have_key 'Build-Status' }
+ end
+
+ context 'when content-range start is too big' do
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '15-20' }) }
+
+ it { expect(response.status).to eq 416 }
+ it { expect(response.header).to have_key 'Range' }
+ it { expect(response.header['Range']).to eq '0-11' }
+ end
+
+ context 'when content-range start is too small' do
+ let(:headers_with_range) { headers.merge({ 'Content-Range' => '8-20' }) }
+
+ it { expect(response.status).to eq 416 }
+ it { expect(response.header).to have_key 'Range' }
+ it { expect(response.header['Range']).to eq '0-11' }
+ end
+
+ context 'when Content-Range header is missing' do
+ let(:headers_with_range) { headers.merge({}) }
+
+ it { expect(response.status).to eq 400 }
+ end
+
+ context 'when build has been errased' do
+ let(:build) { create(:ci_build, runner_id: runner.id, erased_at: Time.now) }
+
+ it { expect(response.status).to eq 403 }
+ end
+ end
+
context "Artifacts" do
let(:file_upload) { fixture_file_upload(Rails.root + 'spec/fixtures/banana_sample.gif', 'image/gif') }
let(:file_upload2) { fixture_file_upload(Rails.root + 'spec/fixtures/dk.png', 'image/gif') }
diff --git a/spec/services/git_tag_push_service_spec.rb b/spec/services/git_tag_push_service_spec.rb
index cc780587e74..a63656e6268 100644
--- a/spec/services/git_tag_push_service_spec.rb
+++ b/spec/services/git_tag_push_service_spec.rb
@@ -5,19 +5,17 @@ describe GitTagPushService, services: true do
let(:user) { create :user }
let(:project) { create :project }
- let(:service) { GitTagPushService.new }
+ let(:service) { GitTagPushService.new(project, user, oldrev: oldrev, newrev: newrev, ref: ref) }
- before do
- @oldrev = Gitlab::Git::BLANK_SHA
- @newrev = "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" # gitlab-test: git rev-parse refs/tags/v1.1.0
- @ref = 'refs/tags/v1.1.0'
- end
+ let(:oldrev) { Gitlab::Git::BLANK_SHA }
+ let(:newrev) { "8a2a6eb295bb170b34c24c76c49ed0e9b2eaf34b" } # gitlab-test: git rev-parse refs/tags/v1.1.0
+ let(:ref) { 'refs/tags/v1.1.0' }
describe "Git Tag Push Data" do
before do
- service.execute(project, user, @oldrev, @newrev, @ref)
+ service.execute
@push_data = service.push_data
- @tag_name = Gitlab::Git.ref_name(@ref)
+ @tag_name = Gitlab::Git.ref_name(ref)
@tag = project.repository.find_tag(@tag_name)
@commit = project.commit(@tag.target)
end
@@ -25,9 +23,9 @@ describe GitTagPushService, services: true do
subject { @push_data }
it { is_expected.to include(object_kind: 'tag_push') }
- it { is_expected.to include(ref: @ref) }
- it { is_expected.to include(before: @oldrev) }
- it { is_expected.to include(after: @newrev) }
+ it { is_expected.to include(ref: ref) }
+ it { is_expected.to include(before: oldrev) }
+ it { is_expected.to include(after: newrev) }
it { is_expected.to include(message: @tag.message) }
it { is_expected.to include(user_id: user.id) }
it { is_expected.to include(user_name: user.name) }
@@ -80,9 +78,11 @@ describe GitTagPushService, services: true do
describe "Webhooks" do
context "execute webhooks" do
+ let(:service) { GitTagPushService.new(project, user, oldrev: 'oldrev', newrev: 'newrev', ref: 'refs/tags/v1.0.0') }
+
it "when pushing tags" do
expect(project).to receive(:execute_hooks)
- service.execute(project, user, 'oldrev', 'newrev', 'refs/tags/v1.0.0')
+ service.execute
end
end
end
diff --git a/spec/support/project_hook_data_shared_example.rb b/spec/support/project_hook_data_shared_example.rb
index 422083875d7..7dbaa6a6459 100644
--- a/spec/support/project_hook_data_shared_example.rb
+++ b/spec/support/project_hook_data_shared_example.rb
@@ -1,4 +1,4 @@
-RSpec.shared_examples 'project hook data' do |project_key: :project|
+RSpec.shared_examples 'project hook data with deprecateds' do |project_key: :project|
it 'contains project data' do
expect(data[project_key][:name]).to eq(project.name)
expect(data[project_key][:description]).to eq(project.description)
@@ -17,6 +17,21 @@ RSpec.shared_examples 'project hook data' do |project_key: :project|
end
end
+RSpec.shared_examples 'project hook data' do |project_key: :project|
+ it 'contains project data' do
+ expect(data[project_key][:name]).to eq(project.name)
+ expect(data[project_key][:description]).to eq(project.description)
+ expect(data[project_key][:web_url]).to eq(project.web_url)
+ expect(data[project_key][:avatar_url]).to eq(project.avatar_url)
+ expect(data[project_key][:git_http_url]).to eq(project.http_url_to_repo)
+ expect(data[project_key][:git_ssh_url]).to eq(project.ssh_url_to_repo)
+ expect(data[project_key][:namespace]).to eq(project.namespace.name)
+ expect(data[project_key][:visibility_level]).to eq(project.visibility_level)
+ expect(data[project_key][:path_with_namespace]).to eq(project.path_with_namespace)
+ expect(data[project_key][:default_branch]).to eq(project.default_branch)
+ end
+end
+
RSpec.shared_examples 'deprecated repository hook data' do |project_key: :project|
it 'contains deprecated repository data' do
expect(data[:repository][:name]).to eq(project.name)
diff --git a/spec/support/repo_helpers.rb b/spec/support/repo_helpers.rb
index aa8258d6dad..73f375c481b 100644
--- a/spec/support/repo_helpers.rb
+++ b/spec/support/repo_helpers.rb
@@ -42,7 +42,7 @@ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
eos
)
end
-
+
def another_sample_commit
OpenStruct.new(
id: "e56497bb5f03a90a51293fc6d516788730953899",
diff --git a/vendor/assets/javascripts/raven.js b/vendor/assets/javascripts/raven.js
new file mode 100644
index 00000000000..d99c6f1c2c8
--- /dev/null
+++ b/vendor/assets/javascripts/raven.js
@@ -0,0 +1,2435 @@
+/*! Raven.js 2.3.0 (b09d766) | github.com/getsentry/raven-js */
+
+/*
+ * Includes TraceKit
+ * https://github.com/getsentry/TraceKit
+ *
+ * Copyright 2016 Matt Robenolt and other contributors
+ * Released under the BSD license
+ * https://github.com/getsentry/raven-js/blob/master/LICENSE
+ *
+ */
+
+(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.Raven = f()}})(function(){var define,module,exports;return (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o<r.length;o++)s(r[o]);return s})({1:[function(_dereq_,module,exports){
+'use strict';
+
+function RavenConfigError(message) {
+ this.name = 'RavenConfigError';
+ this.message = message;
+}
+RavenConfigError.prototype = new Error();
+RavenConfigError.prototype.constructor = RavenConfigError;
+
+module.exports = RavenConfigError;
+
+},{}],2:[function(_dereq_,module,exports){
+/*global XDomainRequest:false*/
+'use strict';
+
+var TraceKit = _dereq_(5);
+var RavenConfigError = _dereq_(1);
+var utils = _dereq_(4);
+
+var isFunction = utils.isFunction;
+var isUndefined = utils.isUndefined;
+var isError = utils.isError;
+var isEmptyObject = utils.isEmptyObject;
+var hasKey = utils.hasKey;
+var joinRegExp = utils.joinRegExp;
+var each = utils.each;
+var objectMerge = utils.objectMerge;
+var truncate = utils.truncate;
+var urlencode = utils.urlencode;
+var uuid4 = utils.uuid4;
+
+var dsnKeys = 'source protocol user pass host port path'.split(' '),
+ dsnPattern = /^(?:(\w+):)?\/\/(?:(\w+)(:\w+)?@)?([\w\.-]+)(?::(\d+))?(\/.*)/;
+
+function now() {
+ return +new Date();
+}
+
+// First, check for JSON support
+// If there is no JSON, we no-op the core features of Raven
+// since JSON is required to encode the payload
+function Raven() {
+ this._hasJSON = !!(typeof JSON === 'object' && JSON.stringify);
+ // Raven can run in contexts where there's no document (react-native)
+ this._hasDocument = typeof document !== 'undefined';
+ this._lastCapturedException = null;
+ this._lastEventId = null;
+ this._globalServer = null;
+ this._globalKey = null;
+ this._globalProject = null;
+ this._globalContext = {};
+ this._globalOptions = {
+ logger: 'javascript',
+ ignoreErrors: [],
+ ignoreUrls: [],
+ whitelistUrls: [],
+ includePaths: [],
+ crossOrigin: 'anonymous',
+ collectWindowErrors: true,
+ maxMessageLength: 0,
+ stackTraceLimit: 50
+ };
+ this._ignoreOnError = 0;
+ this._isRavenInstalled = false;
+ this._originalErrorStackTraceLimit = Error.stackTraceLimit;
+ // capture references to window.console *and* all its methods first
+ // before the console plugin has a chance to monkey patch
+ this._originalConsole = window.console || {};
+ this._originalConsoleMethods = {};
+ this._plugins = [];
+ this._startTime = now();
+ this._wrappedBuiltIns = [];
+
+ for (var method in this._originalConsole) { // eslint-disable-line guard-for-in
+ this._originalConsoleMethods[method] = this._originalConsole[method];
+ }
+}
+
+/*
+ * The core Raven singleton
+ *
+ * @this {Raven}
+ */
+
+Raven.prototype = {
+ // Hardcode version string so that raven source can be loaded directly via
+ // webpack (using a build step causes webpack #1617). Grunt verifies that
+ // this value matches package.json during build.
+ // See: https://github.com/getsentry/raven-js/issues/465
+ VERSION: '2.3.0',
+
+ debug: false,
+
+ TraceKit: TraceKit, // alias to TraceKit
+
+ /*
+ * Configure Raven with a DSN and extra options
+ *
+ * @param {string} dsn The public Sentry DSN
+ * @param {object} options Optional set of of global options [optional]
+ * @return {Raven}
+ */
+ config: function(dsn, options) {
+ var self = this;
+
+ if (this._globalServer) {
+ this._logDebug('error', 'Error: Raven has already been configured');
+ return this;
+ }
+ if (!dsn) return this;
+
+ // merge in options
+ if (options) {
+ each(options, function(key, value){
+ // tags and extra are special and need to be put into context
+ if (key === 'tags' || key === 'extra') {
+ self._globalContext[key] = value;
+ } else {
+ self._globalOptions[key] = value;
+ }
+ });
+ }
+
+ var uri = this._parseDSN(dsn),
+ lastSlash = uri.path.lastIndexOf('/'),
+ path = uri.path.substr(1, lastSlash);
+
+ this._dsn = dsn;
+
+ // "Script error." is hard coded into browsers for errors that it can't read.
+ // this is the result of a script being pulled in from an external domain and CORS.
+ this._globalOptions.ignoreErrors.push(/^Script error\.?$/);
+ this._globalOptions.ignoreErrors.push(/^Javascript error: Script error\.? on line 0$/);
+
+ // join regexp rules into one big rule
+ this._globalOptions.ignoreErrors = joinRegExp(this._globalOptions.ignoreErrors);
+ this._globalOptions.ignoreUrls = this._globalOptions.ignoreUrls.length ? joinRegExp(this._globalOptions.ignoreUrls) : false;
+ this._globalOptions.whitelistUrls = this._globalOptions.whitelistUrls.length ? joinRegExp(this._globalOptions.whitelistUrls) : false;
+ this._globalOptions.includePaths = joinRegExp(this._globalOptions.includePaths);
+
+ this._globalKey = uri.user;
+ this._globalSecret = uri.pass && uri.pass.substr(1);
+ this._globalProject = uri.path.substr(lastSlash + 1);
+
+ this._globalServer = this._getGlobalServer(uri);
+
+ this._globalEndpoint = this._globalServer +
+ '/' + path + 'api/' + this._globalProject + '/store/';
+
+ if (this._globalOptions.fetchContext) {
+ TraceKit.remoteFetching = true;
+ }
+
+ if (this._globalOptions.linesOfContext) {
+ TraceKit.linesOfContext = this._globalOptions.linesOfContext;
+ }
+
+ TraceKit.collectWindowErrors = !!this._globalOptions.collectWindowErrors;
+
+ // return for chaining
+ return this;
+ },
+
+ /*
+ * Installs a global window.onerror error handler
+ * to capture and report uncaught exceptions.
+ * At this point, install() is required to be called due
+ * to the way TraceKit is set up.
+ *
+ * @return {Raven}
+ */
+ install: function() {
+ var self = this;
+ if (this.isSetup() && !this._isRavenInstalled) {
+ TraceKit.report.subscribe(function () {
+ self._handleOnErrorStackInfo.apply(self, arguments);
+ });
+ this._wrapBuiltIns();
+
+ // Install all of the plugins
+ this._drainPlugins();
+
+ this._isRavenInstalled = true;
+ }
+
+ Error.stackTraceLimit = this._globalOptions.stackTraceLimit;
+ return this;
+ },
+
+ /*
+ * Wrap code within a context so Raven can capture errors
+ * reliably across domains that is executed immediately.
+ *
+ * @param {object} options A specific set of options for this context [optional]
+ * @param {function} func The callback to be immediately executed within the context
+ * @param {array} args An array of arguments to be called with the callback [optional]
+ */
+ context: function(options, func, args) {
+ if (isFunction(options)) {
+ args = func || [];
+ func = options;
+ options = undefined;
+ }
+
+ return this.wrap(options, func).apply(this, args);
+ },
+
+ /*
+ * Wrap code within a context and returns back a new function to be executed
+ *
+ * @param {object} options A specific set of options for this context [optional]
+ * @param {function} func The function to be wrapped in a new context
+ * @return {function} The newly wrapped functions with a context
+ */
+ wrap: function(options, func) {
+ var self = this;
+
+ // 1 argument has been passed, and it's not a function
+ // so just return it
+ if (isUndefined(func) && !isFunction(options)) {
+ return options;
+ }
+
+ // options is optional
+ if (isFunction(options)) {
+ func = options;
+ options = undefined;
+ }
+
+ // At this point, we've passed along 2 arguments, and the second one
+ // is not a function either, so we'll just return the second argument.
+ if (!isFunction(func)) {
+ return func;
+ }
+
+ // We don't wanna wrap it twice!
+ try {
+ if (func.__raven__) {
+ return func;
+ }
+ } catch (e) {
+ // Just accessing the __raven__ prop in some Selenium environments
+ // can cause a "Permission denied" exception (see raven-js#495).
+ // Bail on wrapping and return the function as-is (defers to window.onerror).
+ return func;
+ }
+
+ // If this has already been wrapped in the past, return that
+ if (func.__raven_wrapper__ ){
+ return func.__raven_wrapper__ ;
+ }
+
+ function wrapped() {
+ var args = [], i = arguments.length,
+ deep = !options || options && options.deep !== false;
+ // Recursively wrap all of a function's arguments that are
+ // functions themselves.
+
+ while(i--) args[i] = deep ? self.wrap(options, arguments[i]) : arguments[i];
+
+ try {
+ return func.apply(this, args);
+ } catch(e) {
+ self._ignoreNextOnError();
+ self.captureException(e, options);
+ throw e;
+ }
+ }
+
+ // copy over properties of the old function
+ for (var property in func) {
+ if (hasKey(func, property)) {
+ wrapped[property] = func[property];
+ }
+ }
+ func.__raven_wrapper__ = wrapped;
+
+ wrapped.prototype = func.prototype;
+
+ // Signal that this function has been wrapped already
+ // for both debugging and to prevent it to being wrapped twice
+ wrapped.__raven__ = true;
+ wrapped.__inner__ = func;
+
+ return wrapped;
+ },
+
+ /*
+ * Uninstalls the global error handler.
+ *
+ * @return {Raven}
+ */
+ uninstall: function() {
+ TraceKit.report.uninstall();
+
+ this._restoreBuiltIns();
+
+ Error.stackTraceLimit = this._originalErrorStackTraceLimit;
+ this._isRavenInstalled = false;
+
+ return this;
+ },
+
+ /*
+ * Manually capture an exception and send it over to Sentry
+ *
+ * @param {error} ex An exception to be logged
+ * @param {object} options A specific set of options for this error [optional]
+ * @return {Raven}
+ */
+ captureException: function(ex, options) {
+ // If not an Error is passed through, recall as a message instead
+ if (!isError(ex)) return this.captureMessage(ex, options);
+
+ // Store the raw exception object for potential debugging and introspection
+ this._lastCapturedException = ex;
+
+ // TraceKit.report will re-raise any exception passed to it,
+ // which means you have to wrap it in try/catch. Instead, we
+ // can wrap it here and only re-raise if TraceKit.report
+ // raises an exception different from the one we asked to
+ // report on.
+ try {
+ var stack = TraceKit.computeStackTrace(ex);
+ this._handleStackInfo(stack, options);
+ } catch(ex1) {
+ if(ex !== ex1) {
+ throw ex1;
+ }
+ }
+
+ return this;
+ },
+
+ /*
+ * Manually send a message to Sentry
+ *
+ * @param {string} msg A plain message to be captured in Sentry
+ * @param {object} options A specific set of options for this message [optional]
+ * @return {Raven}
+ */
+ captureMessage: function(msg, options) {
+ // config() automagically converts ignoreErrors from a list to a RegExp so we need to test for an
+ // early call; we'll error on the side of logging anything called before configuration since it's
+ // probably something you should see:
+ if (!!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(msg)) {
+ return;
+ }
+
+ // Fire away!
+ this._send(
+ objectMerge({
+ message: msg + '' // Make sure it's actually a string
+ }, options)
+ );
+
+ return this;
+ },
+
+ addPlugin: function(plugin /*arg1, arg2, ... argN*/) {
+ var pluginArgs = Array.prototype.slice.call(arguments, 1);
+
+ this._plugins.push([plugin, pluginArgs]);
+ if (this._isRavenInstalled) {
+ this._drainPlugins();
+ }
+
+ return this;
+ },
+
+ /*
+ * Set/clear a user to be sent along with the payload.
+ *
+ * @param {object} user An object representing user data [optional]
+ * @return {Raven}
+ */
+ setUserContext: function(user) {
+ // Intentionally do not merge here since that's an unexpected behavior.
+ this._globalContext.user = user;
+
+ return this;
+ },
+
+ /*
+ * Merge extra attributes to be sent along with the payload.
+ *
+ * @param {object} extra An object representing extra data [optional]
+ * @return {Raven}
+ */
+ setExtraContext: function(extra) {
+ this._mergeContext('extra', extra);
+
+ return this;
+ },
+
+ /*
+ * Merge tags to be sent along with the payload.
+ *
+ * @param {object} tags An object representing tags [optional]
+ * @return {Raven}
+ */
+ setTagsContext: function(tags) {
+ this._mergeContext('tags', tags);
+
+ return this;
+ },
+
+ /*
+ * Clear all of the context.
+ *
+ * @return {Raven}
+ */
+ clearContext: function() {
+ this._globalContext = {};
+
+ return this;
+ },
+
+ /*
+ * Get a copy of the current context. This cannot be mutated.
+ *
+ * @return {object} copy of context
+ */
+ getContext: function() {
+ // lol javascript
+ return JSON.parse(JSON.stringify(this._globalContext));
+ },
+
+ /*
+ * Set release version of application
+ *
+ * @param {string} release Typically something like a git SHA to identify version
+ * @return {Raven}
+ */
+ setRelease: function(release) {
+ this._globalOptions.release = release;
+
+ return this;
+ },
+
+ /*
+ * Set the dataCallback option
+ *
+ * @param {function} callback The callback to run which allows the
+ * data blob to be mutated before sending
+ * @return {Raven}
+ */
+ setDataCallback: function(callback) {
+ this._globalOptions.dataCallback = callback;
+
+ return this;
+ },
+
+ /*
+ * Set the shouldSendCallback option
+ *
+ * @param {function} callback The callback to run which allows
+ * introspecting the blob before sending
+ * @return {Raven}
+ */
+ setShouldSendCallback: function(callback) {
+ this._globalOptions.shouldSendCallback = callback;
+
+ return this;
+ },
+
+ /**
+ * Override the default HTTP transport mechanism that transmits data
+ * to the Sentry server.
+ *
+ * @param {function} transport Function invoked instead of the default
+ * `makeRequest` handler.
+ *
+ * @return {Raven}
+ */
+ setTransport: function(transport) {
+ this._globalOptions.transport = transport;
+
+ return this;
+ },
+
+ /*
+ * Get the latest raw exception that was captured by Raven.
+ *
+ * @return {error}
+ */
+ lastException: function() {
+ return this._lastCapturedException;
+ },
+
+ /*
+ * Get the last event id
+ *
+ * @return {string}
+ */
+ lastEventId: function() {
+ return this._lastEventId;
+ },
+
+ /*
+ * Determine if Raven is setup and ready to go.
+ *
+ * @return {boolean}
+ */
+ isSetup: function() {
+ if (!this._hasJSON) return false; // needs JSON support
+ if (!this._globalServer) {
+ if (!this.ravenNotConfiguredError) {
+ this.ravenNotConfiguredError = true;
+ this._logDebug('error', 'Error: Raven has not been configured.');
+ }
+ return false;
+ }
+ return true;
+ },
+
+ afterLoad: function () {
+ // TODO: remove window dependence?
+
+ // Attempt to initialize Raven on load
+ var RavenConfig = window.RavenConfig;
+ if (RavenConfig) {
+ this.config(RavenConfig.dsn, RavenConfig.config).install();
+ }
+ },
+
+ showReportDialog: function (options) {
+ if (!window.document) // doesn't work without a document (React native)
+ return;
+
+ options = options || {};
+
+ var lastEventId = options.eventId || this.lastEventId();
+ if (!lastEventId) {
+ throw new RavenConfigError('Missing eventId');
+ }
+
+ var dsn = options.dsn || this._dsn;
+ if (!dsn) {
+ throw new RavenConfigError('Missing DSN');
+ }
+
+ var encode = encodeURIComponent;
+ var qs = '';
+ qs += '?eventId=' + encode(lastEventId);
+ qs += '&dsn=' + encode(dsn);
+
+ var user = options.user || this._globalContext.user;
+ if (user) {
+ if (user.name) qs += '&name=' + encode(user.name);
+ if (user.email) qs += '&email=' + encode(user.email);
+ }
+
+ var globalServer = this._getGlobalServer(this._parseDSN(dsn));
+
+ var script = document.createElement('script');
+ script.async = true;
+ script.src = globalServer + '/api/embed/error-page/' + qs;
+ (document.head || document.body).appendChild(script);
+ },
+
+ /**** Private functions ****/
+ _ignoreNextOnError: function () {
+ var self = this;
+ this._ignoreOnError += 1;
+ setTimeout(function () {
+ // onerror should trigger before setTimeout
+ self._ignoreOnError -= 1;
+ });
+ },
+
+ _triggerEvent: function(eventType, options) {
+ // NOTE: `event` is a native browser thing, so let's avoid conflicting wiht it
+ var evt, key;
+
+ if (!this._hasDocument)
+ return;
+
+ options = options || {};
+
+ eventType = 'raven' + eventType.substr(0,1).toUpperCase() + eventType.substr(1);
+
+ if (document.createEvent) {
+ evt = document.createEvent('HTMLEvents');
+ evt.initEvent(eventType, true, true);
+ } else {
+ evt = document.createEventObject();
+ evt.eventType = eventType;
+ }
+
+ for (key in options) if (hasKey(options, key)) {
+ evt[key] = options[key];
+ }
+
+ if (document.createEvent) {
+ // IE9 if standards
+ document.dispatchEvent(evt);
+ } else {
+ // IE8 regardless of Quirks or Standards
+ // IE9 if quirks
+ try {
+ document.fireEvent('on' + evt.eventType.toLowerCase(), evt);
+ } catch(e) {
+ // Do nothing
+ }
+ }
+ },
+
+ /**
+ * Install any queued plugins
+ */
+ _wrapBuiltIns: function() {
+ var self = this;
+
+ function fill(obj, name, replacement, noUndo) {
+ var orig = obj[name];
+ obj[name] = replacement(orig);
+ if (!noUndo) {
+ self._wrappedBuiltIns.push([obj, name, orig]);
+ }
+ }
+
+ function wrapTimeFn(orig) {
+ return function (fn, t) { // preserve arity
+ // Make a copy of the arguments
+ var args = [].slice.call(arguments);
+ var originalCallback = args[0];
+ if (isFunction(originalCallback)) {
+ args[0] = self.wrap(originalCallback);
+ }
+
+ // IE < 9 doesn't support .call/.apply on setInterval/setTimeout, but it
+ // also supports only two arguments and doesn't care what this is, so we
+ // can just call the original function directly.
+ if (orig.apply) {
+ return orig.apply(this, args);
+ } else {
+ return orig(args[0], args[1]);
+ }
+ };
+ }
+
+ fill(window, 'setTimeout', wrapTimeFn);
+ fill(window, 'setInterval', wrapTimeFn);
+ if (window.requestAnimationFrame) {
+ fill(window, 'requestAnimationFrame', function (orig) {
+ return function (cb) {
+ return orig(self.wrap(cb));
+ };
+ });
+ }
+
+ // event targets borrowed from bugsnag-js:
+ // https://github.com/bugsnag/bugsnag-js/blob/master/src/bugsnag.js#L666
+ 'EventTarget Window Node ApplicationCache AudioTrackList ChannelMergerNode CryptoOperation EventSource FileReader HTMLUnknownElement IDBDatabase IDBRequest IDBTransaction KeyOperation MediaController MessagePort ModalWindow Notification SVGElementInstance Screen TextTrack TextTrackCue TextTrackList WebSocket WebSocketWorker Worker XMLHttpRequest XMLHttpRequestEventTarget XMLHttpRequestUpload'.replace(/\w+/g, function (global) {
+ var proto = window[global] && window[global].prototype;
+ if (proto && proto.hasOwnProperty && proto.hasOwnProperty('addEventListener')) {
+ fill(proto, 'addEventListener', function(orig) {
+ return function (evt, fn, capture, secure) { // preserve arity
+ try {
+ if (fn && fn.handleEvent) {
+ fn.handleEvent = self.wrap(fn.handleEvent);
+ }
+ } catch (err) {
+ // can sometimes get 'Permission denied to access property "handle Event'
+ }
+ return orig.call(this, evt, self.wrap(fn), capture, secure);
+ };
+ });
+ fill(proto, 'removeEventListener', function (orig) {
+ return function (evt, fn, capture, secure) {
+ fn = fn && (fn.__raven_wrapper__ ? fn.__raven_wrapper__ : fn);
+ return orig.call(this, evt, fn, capture, secure);
+ };
+ });
+ }
+ });
+
+ if ('XMLHttpRequest' in window) {
+ fill(XMLHttpRequest.prototype, 'send', function(origSend) {
+ return function (data) { // preserve arity
+ var xhr = this;
+ 'onreadystatechange onload onerror onprogress'.replace(/\w+/g, function (prop) {
+ if (prop in xhr && Object.prototype.toString.call(xhr[prop]) === '[object Function]') {
+ fill(xhr, prop, function (orig) {
+ return self.wrap(orig);
+ }, true /* noUndo */); // don't track filled methods on XHR instances
+ }
+ });
+ return origSend.apply(this, arguments);
+ };
+ });
+ }
+
+ var $ = window.jQuery || window.$;
+ if ($ && $.fn && $.fn.ready) {
+ fill($.fn, 'ready', function (orig) {
+ return function (fn) {
+ return orig.call(this, self.wrap(fn));
+ };
+ });
+ }
+ },
+
+ _restoreBuiltIns: function () {
+ // restore any wrapped builtins
+ var builtin;
+ while (this._wrappedBuiltIns.length) {
+ builtin = this._wrappedBuiltIns.shift();
+
+ var obj = builtin[0],
+ name = builtin[1],
+ orig = builtin[2];
+
+ obj[name] = orig;
+ }
+ },
+
+ _drainPlugins: function() {
+ var self = this;
+
+ // FIX ME TODO
+ each(this._plugins, function(_, plugin) {
+ var installer = plugin[0];
+ var args = plugin[1];
+ installer.apply(self, [self].concat(args));
+ });
+ },
+
+ _parseDSN: function(str) {
+ var m = dsnPattern.exec(str),
+ dsn = {},
+ i = 7;
+
+ try {
+ while (i--) dsn[dsnKeys[i]] = m[i] || '';
+ } catch(e) {
+ throw new RavenConfigError('Invalid DSN: ' + str);
+ }
+
+ if (dsn.pass && !this._globalOptions.allowSecretKey) {
+ throw new RavenConfigError('Do not specify your secret key in the DSN. See: http://bit.ly/raven-secret-key');
+ }
+
+ return dsn;
+ },
+
+ _getGlobalServer: function(uri) {
+ // assemble the endpoint from the uri pieces
+ var globalServer = '//' + uri.host +
+ (uri.port ? ':' + uri.port : '');
+
+ if (uri.protocol) {
+ globalServer = uri.protocol + ':' + globalServer;
+ }
+ return globalServer;
+ },
+
+ _handleOnErrorStackInfo: function() {
+ // if we are intentionally ignoring errors via onerror, bail out
+ if (!this._ignoreOnError) {
+ this._handleStackInfo.apply(this, arguments);
+ }
+ },
+
+ _handleStackInfo: function(stackInfo, options) {
+ var self = this;
+ var frames = [];
+
+ if (stackInfo.stack && stackInfo.stack.length) {
+ each(stackInfo.stack, function(i, stack) {
+ var frame = self._normalizeFrame(stack);
+ if (frame) {
+ frames.push(frame);
+ }
+ });
+ }
+
+ this._triggerEvent('handle', {
+ stackInfo: stackInfo,
+ options: options
+ });
+
+ this._processException(
+ stackInfo.name,
+ stackInfo.message,
+ stackInfo.url,
+ stackInfo.lineno,
+ frames.slice(0, this._globalOptions.stackTraceLimit),
+ options
+ );
+ },
+
+ _normalizeFrame: function(frame) {
+ if (!frame.url) return;
+
+ // normalize the frames data
+ var normalized = {
+ filename: frame.url,
+ lineno: frame.line,
+ colno: frame.column,
+ 'function': frame.func || '?'
+ }, context = this._extractContextFromFrame(frame), i;
+
+ if (context) {
+ var keys = ['pre_context', 'context_line', 'post_context'];
+ i = 3;
+ while (i--) normalized[keys[i]] = context[i];
+ }
+
+ normalized.in_app = !( // determine if an exception came from outside of our app
+ // first we check the global includePaths list.
+ !!this._globalOptions.includePaths.test && !this._globalOptions.includePaths.test(normalized.filename) ||
+ // Now we check for fun, if the function name is Raven or TraceKit
+ /(Raven|TraceKit)\./.test(normalized['function']) ||
+ // finally, we do a last ditch effort and check for raven.min.js
+ /raven\.(min\.)?js$/.test(normalized.filename)
+ );
+
+ return normalized;
+ },
+
+ _extractContextFromFrame: function(frame) {
+ // immediately check if we should even attempt to parse a context
+ if (!frame.context || !this._globalOptions.fetchContext) return;
+
+ var context = frame.context,
+ pivot = ~~(context.length / 2),
+ i = context.length, isMinified = false;
+
+ while (i--) {
+ // We're making a guess to see if the source is minified or not.
+ // To do that, we make the assumption if *any* of the lines passed
+ // in are greater than 300 characters long, we bail.
+ // Sentry will see that there isn't a context
+ if (context[i].length > 300) {
+ isMinified = true;
+ break;
+ }
+ }
+
+ if (isMinified) {
+ // The source is minified and we don't know which column. Fuck it.
+ if (isUndefined(frame.column)) return;
+
+ // If the source is minified and has a frame column
+ // we take a chunk of the offending line to hopefully shed some light
+ return [
+ [], // no pre_context
+ context[pivot].substr(frame.column, 50), // grab 50 characters, starting at the offending column
+ [] // no post_context
+ ];
+ }
+
+ return [
+ context.slice(0, pivot), // pre_context
+ context[pivot], // context_line
+ context.slice(pivot + 1) // post_context
+ ];
+ },
+
+ _processException: function(type, message, fileurl, lineno, frames, options) {
+ var stacktrace, fullMessage;
+
+ if (!!this._globalOptions.ignoreErrors.test && this._globalOptions.ignoreErrors.test(message)) return;
+
+ message += '';
+ message = truncate(message, this._globalOptions.maxMessageLength);
+
+ fullMessage = (type ? type + ': ' : '') + message;
+ fullMessage = truncate(fullMessage, this._globalOptions.maxMessageLength);
+
+ if (frames && frames.length) {
+ fileurl = frames[0].filename || fileurl;
+ // Sentry expects frames oldest to newest
+ // and JS sends them as newest to oldest
+ frames.reverse();
+ stacktrace = {frames: frames};
+ } else if (fileurl) {
+ stacktrace = {
+ frames: [{
+ filename: fileurl,
+ lineno: lineno,
+ in_app: true
+ }]
+ };
+ }
+
+ if (!!this._globalOptions.ignoreUrls.test && this._globalOptions.ignoreUrls.test(fileurl)) return;
+ if (!!this._globalOptions.whitelistUrls.test && !this._globalOptions.whitelistUrls.test(fileurl)) return;
+
+ var data = objectMerge({
+ // sentry.interfaces.Exception
+ exception: {
+ values: [{
+ type: type,
+ value: message,
+ stacktrace: stacktrace
+ }]
+ },
+ culprit: fileurl,
+ message: fullMessage
+ }, options);
+
+ // Fire away!
+ this._send(data);
+ },
+
+ _trimPacket: function(data) {
+ // For now, we only want to truncate the two different messages
+ // but this could/should be expanded to just trim everything
+ var max = this._globalOptions.maxMessageLength;
+ data.message = truncate(data.message, max);
+ if (data.exception) {
+ var exception = data.exception.values[0];
+ exception.value = truncate(exception.value, max);
+ }
+
+ return data;
+ },
+
+ _getHttpData: function() {
+ if (!this._hasDocument || !document.location || !document.location.href) {
+ return;
+ }
+
+ var httpData = {
+ headers: {
+ 'User-Agent': navigator.userAgent
+ }
+ };
+
+ httpData.url = document.location.href;
+
+ if (document.referrer) {
+ httpData.headers.Referer = document.referrer;
+ }
+
+ return httpData;
+ },
+
+
+ _send: function(data) {
+ var self = this;
+
+ var globalOptions = this._globalOptions;
+
+ var baseData = {
+ project: this._globalProject,
+ logger: globalOptions.logger,
+ platform: 'javascript'
+ }, httpData = this._getHttpData();
+
+ if (httpData) {
+ baseData.request = httpData;
+ }
+
+ data = objectMerge(baseData, data);
+
+ // Merge in the tags and extra separately since objectMerge doesn't handle a deep merge
+ data.tags = objectMerge(objectMerge({}, this._globalContext.tags), data.tags);
+ data.extra = objectMerge(objectMerge({}, this._globalContext.extra), data.extra);
+
+ // Send along our own collected metadata with extra
+ data.extra['session:duration'] = now() - this._startTime;
+
+ // If there are no tags/extra, strip the key from the payload alltogther.
+ if (isEmptyObject(data.tags)) delete data.tags;
+
+ if (this._globalContext.user) {
+ // sentry.interfaces.User
+ data.user = this._globalContext.user;
+ }
+
+ // Include the release if it's defined in globalOptions
+ if (globalOptions.release) data.release = globalOptions.release;
+
+ // Include server_name if it's defined in globalOptions
+ if (globalOptions.serverName) data.server_name = globalOptions.serverName;
+
+ if (isFunction(globalOptions.dataCallback)) {
+ data = globalOptions.dataCallback(data) || data;
+ }
+
+ // Why??????????
+ if (!data || isEmptyObject(data)) {
+ return;
+ }
+
+ // Check if the request should be filtered or not
+ if (isFunction(globalOptions.shouldSendCallback) && !globalOptions.shouldSendCallback(data)) {
+ return;
+ }
+
+ // Send along an event_id if not explicitly passed.
+ // This event_id can be used to reference the error within Sentry itself.
+ // Set lastEventId after we know the error should actually be sent
+ this._lastEventId = data.event_id || (data.event_id = uuid4());
+
+ // Try and clean up the packet before sending by truncating long values
+ data = this._trimPacket(data);
+
+ this._logDebug('debug', 'Raven about to send:', data);
+
+ if (!this.isSetup()) return;
+
+ var auth = {
+ sentry_version: '7',
+ sentry_client: 'raven-js/' + this.VERSION,
+ sentry_key: this._globalKey
+ };
+ if (this._globalSecret) {
+ auth.sentry_secret = this._globalSecret;
+ }
+
+ var url = this._globalEndpoint;
+ (globalOptions.transport || this._makeRequest).call(this, {
+ url: url,
+ auth: auth,
+ data: data,
+ options: globalOptions,
+ onSuccess: function success() {
+ self._triggerEvent('success', {
+ data: data,
+ src: url
+ });
+ },
+ onError: function failure() {
+ self._triggerEvent('failure', {
+ data: data,
+ src: url
+ });
+ }
+ });
+ },
+
+ _makeImageRequest: function(opts) {
+ // Tack on sentry_data to auth options, which get urlencoded
+ opts.auth.sentry_data = JSON.stringify(opts.data);
+
+ var img = this._newImage(),
+ src = opts.url + '?' + urlencode(opts.auth),
+ crossOrigin = opts.options.crossOrigin;
+
+ if (crossOrigin || crossOrigin === '') {
+ img.crossOrigin = crossOrigin;
+ }
+ img.onload = opts.onSuccess;
+ img.onerror = img.onabort = opts.onError;
+ img.src = src;
+ },
+
+ _makeXhrRequest: function(opts) {
+ var request;
+
+ var url = opts.url;
+ function handler() {
+ if (request.status === 200) {
+ if (opts.onSuccess) {
+ opts.onSuccess();
+ }
+ } else if (opts.onError) {
+ opts.onError();
+ }
+ }
+
+ request = new XMLHttpRequest();
+ if ('withCredentials' in request) {
+ request.onreadystatechange = function () {
+ if (request.readyState !== 4) {
+ return;
+ }
+ handler();
+ };
+ } else {
+ request = new XDomainRequest();
+ // xdomainrequest cannot go http -> https (or vice versa),
+ // so always use protocol relative
+ url = url.replace(/^https?:/, '');
+
+ // onreadystatechange not supported by XDomainRequest
+ request.onload = handler;
+ }
+
+ // NOTE: auth is intentionally sent as part of query string (NOT as custom
+ // HTTP header) so as to avoid preflight CORS requests
+ request.open('POST', url + '?' + urlencode(opts.auth));
+ request.send(JSON.stringify(opts.data));
+ },
+
+ _makeRequest: function(opts) {
+ var hasCORS =
+ 'withCredentials' in new XMLHttpRequest() ||
+ typeof XDomainRequest !== 'undefined';
+
+ return (hasCORS ? this._makeXhrRequest : this._makeImageRequest)(opts);
+ },
+
+ // Note: this is shitty, but I can't figure out how to get
+ // sinon to stub document.createElement without breaking everything
+ // so this wrapper is just so I can stub it for tests.
+ _newImage: function() {
+ return document.createElement('img');
+ },
+
+ _logDebug: function(level) {
+ if (this._originalConsoleMethods[level] && this.debug) {
+ // In IE<10 console methods do not have their own 'apply' method
+ Function.prototype.apply.call(
+ this._originalConsoleMethods[level],
+ this._originalConsole,
+ [].slice.call(arguments, 1)
+ );
+ }
+ },
+
+ _mergeContext: function(key, context) {
+ if (isUndefined(context)) {
+ delete this._globalContext[key];
+ } else {
+ this._globalContext[key] = objectMerge(this._globalContext[key] || {}, context);
+ }
+ }
+};
+
+// Deprecations
+Raven.prototype.setUser = Raven.prototype.setUserContext;
+Raven.prototype.setReleaseContext = Raven.prototype.setRelease;
+
+module.exports = Raven;
+
+},{"1":1,"4":4,"5":5}],3:[function(_dereq_,module,exports){
+/**
+ * Enforces a single instance of the Raven client, and the
+ * main entry point for Raven. If you are a consumer of the
+ * Raven library, you SHOULD load this file (vs raven.js).
+ **/
+
+'use strict';
+
+var RavenConstructor = _dereq_(2);
+
+var _Raven = window.Raven;
+
+var Raven = new RavenConstructor();
+
+/*
+ * Allow multiple versions of Raven to be installed.
+ * Strip Raven from the global context and returns the instance.
+ *
+ * @return {Raven}
+ */
+Raven.noConflict = function () {
+ window.Raven = _Raven;
+ return Raven;
+};
+
+Raven.afterLoad();
+
+module.exports = Raven;
+
+},{"2":2}],4:[function(_dereq_,module,exports){
+'use strict';
+
+var objectPrototype = Object.prototype;
+
+function isUndefined(what) {
+ return what === void 0;
+}
+
+function isFunction(what) {
+ return typeof what === 'function';
+}
+
+function isString(what) {
+ return objectPrototype.toString.call(what) === '[object String]';
+}
+
+function isObject(what) {
+ return typeof what === 'object' && what !== null;
+}
+
+function isEmptyObject(what) {
+ for (var _ in what) return false; // eslint-disable-line guard-for-in, no-unused-vars
+ return true;
+}
+
+// Sorta yanked from https://github.com/joyent/node/blob/aa3b4b4/lib/util.js#L560
+// with some tiny modifications
+function isError(what) {
+ var toString = objectPrototype.toString.call(what);
+ return isObject(what) &&
+ toString === '[object Error]' ||
+ toString === '[object Exception]' || // Firefox NS_ERROR_FAILURE Exceptions
+ what instanceof Error;
+}
+
+function each(obj, callback) {
+ var i, j;
+
+ if (isUndefined(obj.length)) {
+ for (i in obj) {
+ if (hasKey(obj, i)) {
+ callback.call(null, i, obj[i]);
+ }
+ }
+ } else {
+ j = obj.length;
+ if (j) {
+ for (i = 0; i < j; i++) {
+ callback.call(null, i, obj[i]);
+ }
+ }
+ }
+}
+
+function objectMerge(obj1, obj2) {
+ if (!obj2) {
+ return obj1;
+ }
+ each(obj2, function(key, value){
+ obj1[key] = value;
+ });
+ return obj1;
+}
+
+function truncate(str, max) {
+ return !max || str.length <= max ? str : str.substr(0, max) + '\u2026';
+}
+
+/**
+ * hasKey, a better form of hasOwnProperty
+ * Example: hasKey(MainHostObject, property) === true/false
+ *
+ * @param {Object} host object to check property
+ * @param {string} key to check
+ */
+function hasKey(object, key) {
+ return objectPrototype.hasOwnProperty.call(object, key);
+}
+
+function joinRegExp(patterns) {
+ // Combine an array of regular expressions and strings into one large regexp
+ // Be mad.
+ var sources = [],
+ i = 0, len = patterns.length,
+ pattern;
+
+ for (; i < len; i++) {
+ pattern = patterns[i];
+ if (isString(pattern)) {
+ // If it's a string, we need to escape it
+ // Taken from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions
+ sources.push(pattern.replace(/([.*+?^=!:${}()|\[\]\/\\])/g, '\\$1'));
+ } else if (pattern && pattern.source) {
+ // If it's a regexp already, we want to extract the source
+ sources.push(pattern.source);
+ }
+ // Intentionally skip other cases
+ }
+ return new RegExp(sources.join('|'), 'i');
+}
+
+function urlencode(o) {
+ var pairs = [];
+ each(o, function(key, value) {
+ pairs.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
+ });
+ return pairs.join('&');
+}
+
+function uuid4() {
+ var crypto = window.crypto || window.msCrypto;
+
+ if (!isUndefined(crypto) && crypto.getRandomValues) {
+ // Use window.crypto API if available
+ var arr = new Uint16Array(8);
+ crypto.getRandomValues(arr);
+
+ // set 4 in byte 7
+ arr[3] = arr[3] & 0xFFF | 0x4000;
+ // set 2 most significant bits of byte 9 to '10'
+ arr[4] = arr[4] & 0x3FFF | 0x8000;
+
+ var pad = function(num) {
+ var v = num.toString(16);
+ while (v.length < 4) {
+ v = '0' + v;
+ }
+ return v;
+ };
+
+ return pad(arr[0]) + pad(arr[1]) + pad(arr[2]) + pad(arr[3]) + pad(arr[4]) +
+ pad(arr[5]) + pad(arr[6]) + pad(arr[7]);
+ } else {
+ // http://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
+ return 'xxxxxxxxxxxx4xxxyxxxxxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
+ var r = Math.random()*16|0,
+ v = c === 'x' ? r : r&0x3|0x8;
+ return v.toString(16);
+ });
+ }
+}
+
+module.exports = {
+ isUndefined: isUndefined,
+ isFunction: isFunction,
+ isString: isString,
+ isObject: isObject,
+ isEmptyObject: isEmptyObject,
+ isError: isError,
+ each: each,
+ objectMerge: objectMerge,
+ truncate: truncate,
+ hasKey: hasKey,
+ joinRegExp: joinRegExp,
+ urlencode: urlencode,
+ uuid4: uuid4
+};
+
+},{}],5:[function(_dereq_,module,exports){
+'use strict';
+
+var utils = _dereq_(4);
+
+var hasKey = utils.hasKey;
+var isString = utils.isString;
+var isUndefined = utils.isUndefined;
+
+/*
+ TraceKit - Cross brower stack traces - github.com/occ/TraceKit
+ MIT license
+*/
+
+var TraceKit = {
+ remoteFetching: false,
+ collectWindowErrors: true,
+ // 3 lines before, the offending line, 3 lines after
+ linesOfContext: 7,
+ debug: false
+};
+
+// global reference to slice
+var _slice = [].slice;
+var UNKNOWN_FUNCTION = '?';
+
+// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error#Error_types
+var ERROR_TYPES_RE = /^(?:Uncaught )?((?:Eval|Internal|Range|Reference|Syntax|Type|URI)Error)\: ?(.*)$/;
+
+function getLocationHref() {
+ if (typeof document === 'undefined')
+ return '';
+
+ return document.location.href;
+}
+
+/**
+ * TraceKit.report: cross-browser processing of unhandled exceptions
+ *
+ * Syntax:
+ * TraceKit.report.subscribe(function(stackInfo) { ... })
+ * TraceKit.report.unsubscribe(function(stackInfo) { ... })
+ * TraceKit.report(exception)
+ * try { ...code... } catch(ex) { TraceKit.report(ex); }
+ *
+ * Supports:
+ * - Firefox: full stack trace with line numbers, plus column number
+ * on top frame; column number is not guaranteed
+ * - Opera: full stack trace with line and column numbers
+ * - Chrome: full stack trace with line and column numbers
+ * - Safari: line and column number for the top frame only; some frames
+ * may be missing, and column number is not guaranteed
+ * - IE: line and column number for the top frame only; some frames
+ * may be missing, and column number is not guaranteed
+ *
+ * In theory, TraceKit should work on all of the following versions:
+ * - IE5.5+ (only 8.0 tested)
+ * - Firefox 0.9+ (only 3.5+ tested)
+ * - Opera 7+ (only 10.50 tested; versions 9 and earlier may require
+ * Exceptions Have Stacktrace to be enabled in opera:config)
+ * - Safari 3+ (only 4+ tested)
+ * - Chrome 1+ (only 5+ tested)
+ * - Konqueror 3.5+ (untested)
+ *
+ * Requires TraceKit.computeStackTrace.
+ *
+ * Tries to catch all unhandled exceptions and report them to the
+ * subscribed handlers. Please note that TraceKit.report will rethrow the
+ * exception. This is REQUIRED in order to get a useful stack trace in IE.
+ * If the exception does not reach the top of the browser, you will only
+ * get a stack trace from the point where TraceKit.report was called.
+ *
+ * Handlers receive a stackInfo object as described in the
+ * TraceKit.computeStackTrace docs.
+ */
+TraceKit.report = (function reportModuleWrapper() {
+ var handlers = [],
+ lastArgs = null,
+ lastException = null,
+ lastExceptionStack = null;
+
+ /**
+ * Add a crash handler.
+ * @param {Function} handler
+ */
+ function subscribe(handler) {
+ installGlobalHandler();
+ handlers.push(handler);
+ }
+
+ /**
+ * Remove a crash handler.
+ * @param {Function} handler
+ */
+ function unsubscribe(handler) {
+ for (var i = handlers.length - 1; i >= 0; --i) {
+ if (handlers[i] === handler) {
+ handlers.splice(i, 1);
+ }
+ }
+ }
+
+ /**
+ * Remove all crash handlers.
+ */
+ function unsubscribeAll() {
+ uninstallGlobalHandler();
+ handlers = [];
+ }
+
+ /**
+ * Dispatch stack information to all handlers.
+ * @param {Object.<string, *>} stack
+ */
+ function notifyHandlers(stack, isWindowError) {
+ var exception = null;
+ if (isWindowError && !TraceKit.collectWindowErrors) {
+ return;
+ }
+ for (var i in handlers) {
+ if (hasKey(handlers, i)) {
+ try {
+ handlers[i].apply(null, [stack].concat(_slice.call(arguments, 2)));
+ } catch (inner) {
+ exception = inner;
+ }
+ }
+ }
+
+ if (exception) {
+ throw exception;
+ }
+ }
+
+ var _oldOnerrorHandler, _onErrorHandlerInstalled;
+
+ /**
+ * Ensures all global unhandled exceptions are recorded.
+ * Supported by Gecko and IE.
+ * @param {string} message Error message.
+ * @param {string} url URL of script that generated the exception.
+ * @param {(number|string)} lineNo The line number at which the error
+ * occurred.
+ * @param {?(number|string)} colNo The column number at which the error
+ * occurred.
+ * @param {?Error} ex The actual Error object.
+ */
+ function traceKitWindowOnError(message, url, lineNo, colNo, ex) {
+ var stack = null;
+
+ if (lastExceptionStack) {
+ TraceKit.computeStackTrace.augmentStackTraceWithInitialElement(lastExceptionStack, url, lineNo, message);
+ processLastException();
+ } else if (ex) {
+ // New chrome and blink send along a real error object
+ // Let's just report that like a normal error.
+ // See: https://mikewest.org/2013/08/debugging-runtime-errors-with-window-onerror
+ stack = TraceKit.computeStackTrace(ex);
+ notifyHandlers(stack, true);
+ } else {
+ var location = {
+ 'url': url,
+ 'line': lineNo,
+ 'column': colNo
+ };
+ location.func = TraceKit.computeStackTrace.guessFunctionName(location.url, location.line);
+ location.context = TraceKit.computeStackTrace.gatherContext(location.url, location.line);
+
+ var name = undefined;
+ var msg = message; // must be new var or will modify original `arguments`
+ var groups;
+ if (isString(message)) {
+ var groups = message.match(ERROR_TYPES_RE);
+ if (groups) {
+ name = groups[1];
+ msg = groups[2];
+ }
+ }
+
+ stack = {
+ 'name': name,
+ 'message': msg,
+ 'url': getLocationHref(),
+ 'stack': [location]
+ };
+ notifyHandlers(stack, true);
+ }
+
+ if (_oldOnerrorHandler) {
+ return _oldOnerrorHandler.apply(this, arguments);
+ }
+
+ return false;
+ }
+
+ function installGlobalHandler ()
+ {
+ if (_onErrorHandlerInstalled) {
+ return;
+ }
+ _oldOnerrorHandler = window.onerror;
+ window.onerror = traceKitWindowOnError;
+ _onErrorHandlerInstalled = true;
+ }
+
+ function uninstallGlobalHandler ()
+ {
+ if (!_onErrorHandlerInstalled) {
+ return;
+ }
+ window.onerror = _oldOnerrorHandler;
+ _onErrorHandlerInstalled = false;
+ _oldOnerrorHandler = undefined;
+ }
+
+ function processLastException() {
+ var _lastExceptionStack = lastExceptionStack,
+ _lastArgs = lastArgs;
+ lastArgs = null;
+ lastExceptionStack = null;
+ lastException = null;
+ notifyHandlers.apply(null, [_lastExceptionStack, false].concat(_lastArgs));
+ }
+
+ /**
+ * Reports an unhandled Error to TraceKit.
+ * @param {Error} ex
+ * @param {?boolean} rethrow If false, do not re-throw the exception.
+ * Only used for window.onerror to not cause an infinite loop of
+ * rethrowing.
+ */
+ function report(ex, rethrow) {
+ var args = _slice.call(arguments, 1);
+ if (lastExceptionStack) {
+ if (lastException === ex) {
+ return; // already caught by an inner catch block, ignore
+ } else {
+ processLastException();
+ }
+ }
+
+ var stack = TraceKit.computeStackTrace(ex);
+ lastExceptionStack = stack;
+ lastException = ex;
+ lastArgs = args;
+
+ // If the stack trace is incomplete, wait for 2 seconds for
+ // slow slow IE to see if onerror occurs or not before reporting
+ // this exception; otherwise, we will end up with an incomplete
+ // stack trace
+ window.setTimeout(function () {
+ if (lastException === ex) {
+ processLastException();
+ }
+ }, (stack.incomplete ? 2000 : 0));
+
+ if (rethrow !== false) {
+ throw ex; // re-throw to propagate to the top level (and cause window.onerror)
+ }
+ }
+
+ report.subscribe = subscribe;
+ report.unsubscribe = unsubscribe;
+ report.uninstall = unsubscribeAll;
+ return report;
+}());
+
+/**
+ * TraceKit.computeStackTrace: cross-browser stack traces in JavaScript
+ *
+ * Syntax:
+ * s = TraceKit.computeStackTrace(exception) // consider using TraceKit.report instead (see below)
+ * Returns:
+ * s.name - exception name
+ * s.message - exception message
+ * s.stack[i].url - JavaScript or HTML file URL
+ * s.stack[i].func - function name, or empty for anonymous functions (if guessing did not work)
+ * s.stack[i].args - arguments passed to the function, if known
+ * s.stack[i].line - line number, if known
+ * s.stack[i].column - column number, if known
+ * s.stack[i].context - an array of source code lines; the middle element corresponds to the correct line#
+ *
+ * Supports:
+ * - Firefox: full stack trace with line numbers and unreliable column
+ * number on top frame
+ * - Opera 10: full stack trace with line and column numbers
+ * - Opera 9-: full stack trace with line numbers
+ * - Chrome: full stack trace with line and column numbers
+ * - Safari: line and column number for the topmost stacktrace element
+ * only
+ * - IE: no line numbers whatsoever
+ *
+ * Tries to guess names of anonymous functions by looking for assignments
+ * in the source code. In IE and Safari, we have to guess source file names
+ * by searching for function bodies inside all page scripts. This will not
+ * work for scripts that are loaded cross-domain.
+ * Here be dragons: some function names may be guessed incorrectly, and
+ * duplicate functions may be mismatched.
+ *
+ * TraceKit.computeStackTrace should only be used for tracing purposes.
+ * Logging of unhandled exceptions should be done with TraceKit.report,
+ * which builds on top of TraceKit.computeStackTrace and provides better
+ * IE support by utilizing the window.onerror event to retrieve information
+ * about the top of the stack.
+ *
+ * Note: In IE and Safari, no stack trace is recorded on the Error object,
+ * so computeStackTrace instead walks its *own* chain of callers.
+ * This means that:
+ * * in Safari, some methods may be missing from the stack trace;
+ * * in IE, the topmost function in the stack trace will always be the
+ * caller of computeStackTrace.
+ *
+ * This is okay for tracing (because you are likely to be calling
+ * computeStackTrace from the function you want to be the topmost element
+ * of the stack trace anyway), but not okay for logging unhandled
+ * exceptions (because your catch block will likely be far away from the
+ * inner function that actually caused the exception).
+ *
+ */
+TraceKit.computeStackTrace = (function computeStackTraceWrapper() {
+ var sourceCache = {};
+
+ /**
+ * Attempts to retrieve source code via XMLHttpRequest, which is used
+ * to look up anonymous function names.
+ * @param {string} url URL of source code.
+ * @return {string} Source contents.
+ */
+ function loadSource(url) {
+ if (!TraceKit.remoteFetching) { //Only attempt request if remoteFetching is on.
+ return '';
+ }
+ try {
+ var getXHR = function() {
+ try {
+ return new window.XMLHttpRequest();
+ } catch (e) {
+ // explicitly bubble up the exception if not found
+ return new window.ActiveXObject('Microsoft.XMLHTTP');
+ }
+ };
+
+ var request = getXHR();
+ request.open('GET', url, false);
+ request.send('');
+ return request.responseText;
+ } catch (e) {
+ return '';
+ }
+ }
+
+ /**
+ * Retrieves source code from the source code cache.
+ * @param {string} url URL of source code.
+ * @return {Array.<string>} Source contents.
+ */
+ function getSource(url) {
+ if (!isString(url)) return [];
+ if (!hasKey(sourceCache, url)) {
+ // URL needs to be able to fetched within the acceptable domain. Otherwise,
+ // cross-domain errors will be triggered.
+ var source = '';
+ var domain = '';
+ try { domain = document.domain; } catch (e) {}
+ if (url.indexOf(domain) !== -1) {
+ source = loadSource(url);
+ }
+ sourceCache[url] = source ? source.split('\n') : [];
+ }
+
+ return sourceCache[url];
+ }
+
+ /**
+ * Tries to use an externally loaded copy of source code to determine
+ * the name of a function by looking at the name of the variable it was
+ * assigned to, if any.
+ * @param {string} url URL of source code.
+ * @param {(string|number)} lineNo Line number in source code.
+ * @return {string} The function name, if discoverable.
+ */
+ function guessFunctionName(url, lineNo) {
+ var reFunctionArgNames = /function ([^(]*)\(([^)]*)\)/,
+ reGuessFunction = /['"]?([0-9A-Za-z$_]+)['"]?\s*[:=]\s*(function|eval|new Function)/,
+ line = '',
+ maxLines = 10,
+ source = getSource(url),
+ m;
+
+ if (!source.length) {
+ return UNKNOWN_FUNCTION;
+ }
+
+ // Walk backwards from the first line in the function until we find the line which
+ // matches the pattern above, which is the function definition
+ for (var i = 0; i < maxLines; ++i) {
+ line = source[lineNo - i] + line;
+
+ if (!isUndefined(line)) {
+ if ((m = reGuessFunction.exec(line))) {
+ return m[1];
+ } else if ((m = reFunctionArgNames.exec(line))) {
+ return m[1];
+ }
+ }
+ }
+
+ return UNKNOWN_FUNCTION;
+ }
+
+ /**
+ * Retrieves the surrounding lines from where an exception occurred.
+ * @param {string} url URL of source code.
+ * @param {(string|number)} line Line number in source code to centre
+ * around for context.
+ * @return {?Array.<string>} Lines of source code.
+ */
+ function gatherContext(url, line) {
+ var source = getSource(url);
+
+ if (!source.length) {
+ return null;
+ }
+
+ var context = [],
+ // linesBefore & linesAfter are inclusive with the offending line.
+ // if linesOfContext is even, there will be one extra line
+ // *before* the offending line.
+ linesBefore = Math.floor(TraceKit.linesOfContext / 2),
+ // Add one extra line if linesOfContext is odd
+ linesAfter = linesBefore + (TraceKit.linesOfContext % 2),
+ start = Math.max(0, line - linesBefore - 1),
+ end = Math.min(source.length, line + linesAfter - 1);
+
+ line -= 1; // convert to 0-based index
+
+ for (var i = start; i < end; ++i) {
+ if (!isUndefined(source[i])) {
+ context.push(source[i]);
+ }
+ }
+
+ return context.length > 0 ? context : null;
+ }
+
+ /**
+ * Escapes special characters, except for whitespace, in a string to be
+ * used inside a regular expression as a string literal.
+ * @param {string} text The string.
+ * @return {string} The escaped string literal.
+ */
+ function escapeRegExp(text) {
+ return text.replace(/[\-\[\]{}()*+?.,\\\^$|#]/g, '\\$&');
+ }
+
+ /**
+ * Escapes special characters in a string to be used inside a regular
+ * expression as a string literal. Also ensures that HTML entities will
+ * be matched the same as their literal friends.
+ * @param {string} body The string.
+ * @return {string} The escaped string.
+ */
+ function escapeCodeAsRegExpForMatchingInsideHTML(body) {
+ return escapeRegExp(body).replace('<', '(?:<|&lt;)').replace('>', '(?:>|&gt;)').replace('&', '(?:&|&amp;)').replace('"', '(?:"|&quot;)').replace(/\s+/g, '\\s+');
+ }
+
+ /**
+ * Determines where a code fragment occurs in the source code.
+ * @param {RegExp} re The function definition.
+ * @param {Array.<string>} urls A list of URLs to search.
+ * @return {?Object.<string, (string|number)>} An object containing
+ * the url, line, and column number of the defined function.
+ */
+ function findSourceInUrls(re, urls) {
+ var source, m;
+ for (var i = 0, j = urls.length; i < j; ++i) {
+ // console.log('searching', urls[i]);
+ if ((source = getSource(urls[i])).length) {
+ source = source.join('\n');
+ if ((m = re.exec(source))) {
+ // console.log('Found function in ' + urls[i]);
+
+ return {
+ 'url': urls[i],
+ 'line': source.substring(0, m.index).split('\n').length,
+ 'column': m.index - source.lastIndexOf('\n', m.index) - 1
+ };
+ }
+ }
+ }
+
+ // console.log('no match');
+
+ return null;
+ }
+
+ /**
+ * Determines at which column a code fragment occurs on a line of the
+ * source code.
+ * @param {string} fragment The code fragment.
+ * @param {string} url The URL to search.
+ * @param {(string|number)} line The line number to examine.
+ * @return {?number} The column number.
+ */
+ function findSourceInLine(fragment, url, line) {
+ var source = getSource(url),
+ re = new RegExp('\\b' + escapeRegExp(fragment) + '\\b'),
+ m;
+
+ line -= 1;
+
+ if (source && source.length > line && (m = re.exec(source[line]))) {
+ return m.index;
+ }
+
+ return null;
+ }
+
+ /**
+ * Determines where a function was defined within the source code.
+ * @param {(Function|string)} func A function reference or serialized
+ * function definition.
+ * @return {?Object.<string, (string|number)>} An object containing
+ * the url, line, and column number of the defined function.
+ */
+ function findSourceByFunctionBody(func) {
+ if (typeof document === 'undefined')
+ return;
+
+ var urls = [window.location.href],
+ scripts = document.getElementsByTagName('script'),
+ body,
+ code = '' + func,
+ codeRE = /^function(?:\s+([\w$]+))?\s*\(([\w\s,]*)\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/,
+ eventRE = /^function on([\w$]+)\s*\(event\)\s*\{\s*(\S[\s\S]*\S)\s*\}\s*$/,
+ re,
+ parts,
+ result;
+
+ for (var i = 0; i < scripts.length; ++i) {
+ var script = scripts[i];
+ if (script.src) {
+ urls.push(script.src);
+ }
+ }
+
+ if (!(parts = codeRE.exec(code))) {
+ re = new RegExp(escapeRegExp(code).replace(/\s+/g, '\\s+'));
+ }
+
+ // not sure if this is really necessary, but I don’t have a test
+ // corpus large enough to confirm that and it was in the original.
+ else {
+ var name = parts[1] ? '\\s+' + parts[1] : '',
+ args = parts[2].split(',').join('\\s*,\\s*');
+
+ body = escapeRegExp(parts[3]).replace(/;$/, ';?'); // semicolon is inserted if the function ends with a comment.replace(/\s+/g, '\\s+');
+ re = new RegExp('function' + name + '\\s*\\(\\s*' + args + '\\s*\\)\\s*{\\s*' + body + '\\s*}');
+ }
+
+ // look for a normal function definition
+ if ((result = findSourceInUrls(re, urls))) {
+ return result;
+ }
+
+ // look for an old-school event handler function
+ if ((parts = eventRE.exec(code))) {
+ var event = parts[1];
+ body = escapeCodeAsRegExpForMatchingInsideHTML(parts[2]);
+
+ // look for a function defined in HTML as an onXXX handler
+ re = new RegExp('on' + event + '=[\\\'"]\\s*' + body + '\\s*[\\\'"]', 'i');
+
+ if ((result = findSourceInUrls(re, urls[0]))) {
+ return result;
+ }
+
+ // look for ???
+ re = new RegExp(body);
+
+ if ((result = findSourceInUrls(re, urls))) {
+ return result;
+ }
+ }
+
+ return null;
+ }
+
+ // Contents of Exception in various browsers.
+ //
+ // SAFARI:
+ // ex.message = Can't find variable: qq
+ // ex.line = 59
+ // ex.sourceId = 580238192
+ // ex.sourceURL = http://...
+ // ex.expressionBeginOffset = 96
+ // ex.expressionCaretOffset = 98
+ // ex.expressionEndOffset = 98
+ // ex.name = ReferenceError
+ //
+ // FIREFOX:
+ // ex.message = qq is not defined
+ // ex.fileName = http://...
+ // ex.lineNumber = 59
+ // ex.columnNumber = 69
+ // ex.stack = ...stack trace... (see the example below)
+ // ex.name = ReferenceError
+ //
+ // CHROME:
+ // ex.message = qq is not defined
+ // ex.name = ReferenceError
+ // ex.type = not_defined
+ // ex.arguments = ['aa']
+ // ex.stack = ...stack trace...
+ //
+ // INTERNET EXPLORER:
+ // ex.message = ...
+ // ex.name = ReferenceError
+ //
+ // OPERA:
+ // ex.message = ...message... (see the example below)
+ // ex.name = ReferenceError
+ // ex.opera#sourceloc = 11 (pretty much useless, duplicates the info in ex.message)
+ // ex.stacktrace = n/a; see 'opera:config#UserPrefs|Exceptions Have Stacktrace'
+
+ /**
+ * Computes stack trace information from the stack property.
+ * Chrome and Gecko use this property.
+ * @param {Error} ex
+ * @return {?Object.<string, *>} Stack trace information.
+ */
+ function computeStackTraceFromStackProp(ex) {
+ if (isUndefined(ex.stack) || !ex.stack) return;
+
+ var chrome = /^\s*at (.*?) ?\(((?:file|https?|blob|chrome-extension|native|eval|<anonymous>).*?)(?::(\d+))?(?::(\d+))?\)?\s*$/i,
+ gecko = /^\s*(.*?)(?:\((.*?)\))?(?:^|@)((?:file|https?|blob|chrome|\[native).*?)(?::(\d+))?(?::(\d+))?\s*$/i,
+ winjs = /^\s*at (?:((?:\[object object\])?.+) )?\(?((?:ms-appx|https?|blob):.*?):(\d+)(?::(\d+))?\)?\s*$/i,
+ lines = ex.stack.split('\n'),
+ stack = [],
+ parts,
+ element,
+ reference = /^(.*) is undefined$/.exec(ex.message);
+
+ for (var i = 0, j = lines.length; i < j; ++i) {
+ if ((parts = chrome.exec(lines[i]))) {
+ var isNative = parts[2] && parts[2].indexOf('native') !== -1;
+ element = {
+ 'url': !isNative ? parts[2] : null,
+ 'func': parts[1] || UNKNOWN_FUNCTION,
+ 'args': isNative ? [parts[2]] : [],
+ 'line': parts[3] ? +parts[3] : null,
+ 'column': parts[4] ? +parts[4] : null
+ };
+ } else if ( parts = winjs.exec(lines[i]) ) {
+ element = {
+ 'url': parts[2],
+ 'func': parts[1] || UNKNOWN_FUNCTION,
+ 'args': [],
+ 'line': +parts[3],
+ 'column': parts[4] ? +parts[4] : null
+ };
+ } else if ((parts = gecko.exec(lines[i]))) {
+ element = {
+ 'url': parts[3],
+ 'func': parts[1] || UNKNOWN_FUNCTION,
+ 'args': parts[2] ? parts[2].split(',') : [],
+ 'line': parts[4] ? +parts[4] : null,
+ 'column': parts[5] ? +parts[5] : null
+ };
+ } else {
+ continue;
+ }
+
+ if (!element.func && element.line) {
+ element.func = guessFunctionName(element.url, element.line);
+ }
+
+ if (element.line) {
+ element.context = gatherContext(element.url, element.line);
+ }
+
+ stack.push(element);
+ }
+
+ if (!stack.length) {
+ return null;
+ }
+
+ if (stack[0].line && !stack[0].column && reference) {
+ stack[0].column = findSourceInLine(reference[1], stack[0].url, stack[0].line);
+ } else if (!stack[0].column && !isUndefined(ex.columnNumber)) {
+ // FireFox uses this awesome columnNumber property for its top frame
+ // Also note, Firefox's column number is 0-based and everything else expects 1-based,
+ // so adding 1
+ stack[0].column = ex.columnNumber + 1;
+ }
+
+ return {
+ 'name': ex.name,
+ 'message': ex.message,
+ 'url': getLocationHref(),
+ 'stack': stack
+ };
+ }
+
+ /**
+ * Computes stack trace information from the stacktrace property.
+ * Opera 10 uses this property.
+ * @param {Error} ex
+ * @return {?Object.<string, *>} Stack trace information.
+ */
+ function computeStackTraceFromStacktraceProp(ex) {
+ // Access and store the stacktrace property before doing ANYTHING
+ // else to it because Opera is not very good at providing it
+ // reliably in other circumstances.
+ var stacktrace = ex.stacktrace;
+ if (isUndefined(ex.stacktrace) || !ex.stacktrace) return;
+
+ var opera10Regex = / line (\d+).*script (?:in )?(\S+)(?:: in function (\S+))?$/i,
+ opera11Regex = / line (\d+), column (\d+)\s*(?:in (?:<anonymous function: ([^>]+)>|([^\)]+))\((.*)\))? in (.*):\s*$/i,
+ lines = stacktrace.split('\n'),
+ stack = [],
+ parts;
+
+ for (var line = 0; line < lines.length; line += 2) {
+ var element = null;
+ if ((parts = opera10Regex.exec(lines[line]))) {
+ element = {
+ 'url': parts[2],
+ 'line': +parts[1],
+ 'column': null,
+ 'func': parts[3],
+ 'args':[]
+ };
+ } else if ((parts = opera11Regex.exec(lines[line]))) {
+ element = {
+ 'url': parts[6],
+ 'line': +parts[1],
+ 'column': +parts[2],
+ 'func': parts[3] || parts[4],
+ 'args': parts[5] ? parts[5].split(',') : []
+ };
+ }
+
+ if (element) {
+ if (!element.func && element.line) {
+ element.func = guessFunctionName(element.url, element.line);
+ }
+ if (element.line) {
+ try {
+ element.context = gatherContext(element.url, element.line);
+ } catch (exc) {}
+ }
+
+ if (!element.context) {
+ element.context = [lines[line + 1]];
+ }
+
+ stack.push(element);
+ }
+ }
+
+ if (!stack.length) {
+ return null;
+ }
+
+ return {
+ 'name': ex.name,
+ 'message': ex.message,
+ 'url': getLocationHref(),
+ 'stack': stack
+ };
+ }
+
+ /**
+ * NOT TESTED.
+ * Computes stack trace information from an error message that includes
+ * the stack trace.
+ * Opera 9 and earlier use this method if the option to show stack
+ * traces is turned on in opera:config.
+ * @param {Error} ex
+ * @return {?Object.<string, *>} Stack information.
+ */
+ function computeStackTraceFromOperaMultiLineMessage(ex) {
+ // Opera includes a stack trace into the exception message. An example is:
+ //
+ // Statement on line 3: Undefined variable: undefinedFunc
+ // Backtrace:
+ // Line 3 of linked script file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.js: In function zzz
+ // undefinedFunc(a);
+ // Line 7 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function yyy
+ // zzz(x, y, z);
+ // Line 3 of inline#1 script in file://localhost/Users/andreyvit/Projects/TraceKit/javascript-client/sample.html: In function xxx
+ // yyy(a, a, a);
+ // Line 1 of function script
+ // try { xxx('hi'); return false; } catch(ex) { TraceKit.report(ex); }
+ // ...
+
+ var lines = ex.message.split('\n');
+ if (lines.length < 4) {
+ return null;
+ }
+
+ var lineRE1 = /^\s*Line (\d+) of linked script ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i,
+ lineRE2 = /^\s*Line (\d+) of inline#(\d+) script in ((?:file|https?|blob)\S+)(?:: in function (\S+))?\s*$/i,
+ lineRE3 = /^\s*Line (\d+) of function script\s*$/i,
+ stack = [],
+ scripts = document.getElementsByTagName('script'),
+ inlineScriptBlocks = [],
+ parts;
+
+ for (var s in scripts) {
+ if (hasKey(scripts, s) && !scripts[s].src) {
+ inlineScriptBlocks.push(scripts[s]);
+ }
+ }
+
+ for (var line = 2; line < lines.length; line += 2) {
+ var item = null;
+ if ((parts = lineRE1.exec(lines[line]))) {
+ item = {
+ 'url': parts[2],
+ 'func': parts[3],
+ 'args': [],
+ 'line': +parts[1],
+ 'column': null
+ };
+ } else if ((parts = lineRE2.exec(lines[line]))) {
+ item = {
+ 'url': parts[3],
+ 'func': parts[4],
+ 'args': [],
+ 'line': +parts[1],
+ 'column': null // TODO: Check to see if inline#1 (+parts[2]) points to the script number or column number.
+ };
+ var relativeLine = (+parts[1]); // relative to the start of the <SCRIPT> block
+ var script = inlineScriptBlocks[parts[2] - 1];
+ if (script) {
+ var source = getSource(item.url);
+ if (source) {
+ source = source.join('\n');
+ var pos = source.indexOf(script.innerText);
+ if (pos >= 0) {
+ item.line = relativeLine + source.substring(0, pos).split('\n').length;
+ }
+ }
+ }
+ } else if ((parts = lineRE3.exec(lines[line]))) {
+ var url = window.location.href.replace(/#.*$/, '');
+ var re = new RegExp(escapeCodeAsRegExpForMatchingInsideHTML(lines[line + 1]));
+ var src = findSourceInUrls(re, [url]);
+ item = {
+ 'url': url,
+ 'func': '',
+ 'args': [],
+ 'line': src ? src.line : parts[1],
+ 'column': null
+ };
+ }
+
+ if (item) {
+ if (!item.func) {
+ item.func = guessFunctionName(item.url, item.line);
+ }
+ var context = gatherContext(item.url, item.line);
+ var midline = (context ? context[Math.floor(context.length / 2)] : null);
+ if (context && midline.replace(/^\s*/, '') === lines[line + 1].replace(/^\s*/, '')) {
+ item.context = context;
+ } else {
+ // if (context) alert("Context mismatch. Correct midline:\n" + lines[i+1] + "\n\nMidline:\n" + midline + "\n\nContext:\n" + context.join("\n") + "\n\nURL:\n" + item.url);
+ item.context = [lines[line + 1]];
+ }
+ stack.push(item);
+ }
+ }
+
+ if (!stack.length) {
+ return null; // could not parse multiline exception message as Opera stack trace
+ }
+
+ return {
+ 'name': ex.name,
+ 'message': lines[0],
+ 'url': getLocationHref(),
+ 'stack': stack
+ };
+ }
+
+ /**
+ * Adds information about the first frame to incomplete stack traces.
+ * Safari and IE require this to get complete data on the first frame.
+ * @param {Object.<string, *>} stackInfo Stack trace information from
+ * one of the compute* methods.
+ * @param {string} url The URL of the script that caused an error.
+ * @param {(number|string)} lineNo The line number of the script that
+ * caused an error.
+ * @param {string=} message The error generated by the browser, which
+ * hopefully contains the name of the object that caused the error.
+ * @return {boolean} Whether or not the stack information was
+ * augmented.
+ */
+ function augmentStackTraceWithInitialElement(stackInfo, url, lineNo, message) {
+ var initial = {
+ 'url': url,
+ 'line': lineNo
+ };
+
+ if (initial.url && initial.line) {
+ stackInfo.incomplete = false;
+
+ if (!initial.func) {
+ initial.func = guessFunctionName(initial.url, initial.line);
+ }
+
+ if (!initial.context) {
+ initial.context = gatherContext(initial.url, initial.line);
+ }
+
+ var reference = / '([^']+)' /.exec(message);
+ if (reference) {
+ initial.column = findSourceInLine(reference[1], initial.url, initial.line);
+ }
+
+ if (stackInfo.stack.length > 0) {
+ if (stackInfo.stack[0].url === initial.url) {
+ if (stackInfo.stack[0].line === initial.line) {
+ return false; // already in stack trace
+ } else if (!stackInfo.stack[0].line && stackInfo.stack[0].func === initial.func) {
+ stackInfo.stack[0].line = initial.line;
+ stackInfo.stack[0].context = initial.context;
+ return false;
+ }
+ }
+ }
+
+ stackInfo.stack.unshift(initial);
+ stackInfo.partial = true;
+ return true;
+ } else {
+ stackInfo.incomplete = true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Computes stack trace information by walking the arguments.caller
+ * chain at the time the exception occurred. This will cause earlier
+ * frames to be missed but is the only way to get any stack trace in
+ * Safari and IE. The top frame is restored by
+ * {@link augmentStackTraceWithInitialElement}.
+ * @param {Error} ex
+ * @return {?Object.<string, *>} Stack trace information.
+ */
+ function computeStackTraceByWalkingCallerChain(ex, depth) {
+ var functionName = /function\s+([_$a-zA-Z\xA0-\uFFFF][_$a-zA-Z0-9\xA0-\uFFFF]*)?\s*\(/i,
+ stack = [],
+ funcs = {},
+ recursion = false,
+ parts,
+ item,
+ source;
+
+ for (var curr = computeStackTraceByWalkingCallerChain.caller; curr && !recursion; curr = curr.caller) {
+ if (curr === computeStackTrace || curr === TraceKit.report) {
+ // console.log('skipping internal function');
+ continue;
+ }
+
+ item = {
+ 'url': null,
+ 'func': UNKNOWN_FUNCTION,
+ 'line': null,
+ 'column': null
+ };
+
+ if (curr.name) {
+ item.func = curr.name;
+ } else if ((parts = functionName.exec(curr.toString()))) {
+ item.func = parts[1];
+ }
+
+ if (typeof item.func === 'undefined') {
+ try {
+ item.func = parts.input.substring(0, parts.input.indexOf('{'));
+ } catch (e) { }
+ }
+
+ if ((source = findSourceByFunctionBody(curr))) {
+ item.url = source.url;
+ item.line = source.line;
+
+ if (item.func === UNKNOWN_FUNCTION) {
+ item.func = guessFunctionName(item.url, item.line);
+ }
+
+ var reference = / '([^']+)' /.exec(ex.message || ex.description);
+ if (reference) {
+ item.column = findSourceInLine(reference[1], source.url, source.line);
+ }
+ }
+
+ if (funcs['' + curr]) {
+ recursion = true;
+ }else{
+ funcs['' + curr] = true;
+ }
+
+ stack.push(item);
+ }
+
+ if (depth) {
+ // console.log('depth is ' + depth);
+ // console.log('stack is ' + stack.length);
+ stack.splice(0, depth);
+ }
+
+ var result = {
+ 'name': ex.name,
+ 'message': ex.message,
+ 'url': getLocationHref(),
+ 'stack': stack
+ };
+ augmentStackTraceWithInitialElement(result, ex.sourceURL || ex.fileName, ex.line || ex.lineNumber, ex.message || ex.description);
+ return result;
+ }
+
+ /**
+ * Computes a stack trace for an exception.
+ * @param {Error} ex
+ * @param {(string|number)=} depth
+ */
+ function computeStackTrace(ex, depth) {
+ var stack = null;
+ depth = (depth == null ? 0 : +depth);
+
+ try {
+ // This must be tried first because Opera 10 *destroys*
+ // its stacktrace property if you try to access the stack
+ // property first!!
+ stack = computeStackTraceFromStacktraceProp(ex);
+ if (stack) {
+ return stack;
+ }
+ } catch (e) {
+ if (TraceKit.debug) {
+ throw e;
+ }
+ }
+
+ try {
+ stack = computeStackTraceFromStackProp(ex);
+ if (stack) {
+ return stack;
+ }
+ } catch (e) {
+ if (TraceKit.debug) {
+ throw e;
+ }
+ }
+
+ try {
+ stack = computeStackTraceFromOperaMultiLineMessage(ex);
+ if (stack) {
+ return stack;
+ }
+ } catch (e) {
+ if (TraceKit.debug) {
+ throw e;
+ }
+ }
+
+ try {
+ stack = computeStackTraceByWalkingCallerChain(ex, depth + 1);
+ if (stack) {
+ return stack;
+ }
+ } catch (e) {
+ if (TraceKit.debug) {
+ throw e;
+ }
+ }
+
+ return {
+ 'name': ex.name,
+ 'message': ex.message,
+ 'url': getLocationHref()
+ };
+ }
+
+ computeStackTrace.augmentStackTraceWithInitialElement = augmentStackTraceWithInitialElement;
+ computeStackTrace.computeStackTraceFromStackProp = computeStackTraceFromStackProp;
+ computeStackTrace.guessFunctionName = guessFunctionName;
+ computeStackTrace.gatherContext = gatherContext;
+
+ return computeStackTrace;
+}());
+
+module.exports = TraceKit;
+
+},{"4":4}]},{},[3])(3)
+}); \ No newline at end of file