Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Support
Keyboard shortcuts
?
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
G
gitlab-ce
Project overview
Project overview
Details
Activity
Releases
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Issues
0
Issues
0
List
Boards
Labels
Milestones
Merge Requests
0
Merge Requests
0
Analytics
Analytics
Repository
Value Stream
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Create a new issue
Commits
Issue Boards
Open sidebar
iv
gitlab-ce
Commits
7e0ef892
Commit
7e0ef892
authored
Apr 20, 2016
by
Alfredo Sumaran
Browse files
Options
Browse Files
Download
Plain Diff
Merge remote-tracking branch 'origin/master' into label-dropdown-fix
parents
a5290ac2
7e6d5906
Changes
58
Hide whitespace changes
Inline
Side-by-side
Showing
58 changed files
with
3448 additions
and
191 deletions
+3448
-191
CHANGELOG
CHANGELOG
+15
-2
CONTRIBUTING.md
CONTRIBUTING.md
+1
-0
app/assets/javascripts/application.js.coffee
app/assets/javascripts/application.js.coffee
+1
-0
app/assets/javascripts/dispatcher.js.coffee
app/assets/javascripts/dispatcher.js.coffee
+2
-1
app/assets/javascripts/due_date_select.js.coffee
app/assets/javascripts/due_date_select.js.coffee
+64
-0
app/assets/javascripts/gl_dropdown.js.coffee
app/assets/javascripts/gl_dropdown.js.coffee
+4
-4
app/assets/javascripts/issuable.js.coffee
app/assets/javascripts/issuable.js.coffee
+84
-0
app/assets/javascripts/issuable_context.js.coffee
app/assets/javascripts/issuable_context.js.coffee
+0
-1
app/assets/javascripts/issues.js.coffee
app/assets/javascripts/issues.js.coffee
+1
-50
app/assets/javascripts/labels_select.js.coffee
app/assets/javascripts/labels_select.js.coffee
+44
-19
app/assets/javascripts/lib/animate.js.coffee
app/assets/javascripts/lib/animate.js.coffee
+30
-4
app/assets/javascripts/lib/url_utility.js.coffee
app/assets/javascripts/lib/url_utility.js.coffee
+14
-2
app/assets/javascripts/merge_requests.js.coffee
app/assets/javascripts/merge_requests.js.coffee
+0
-35
app/assets/javascripts/milestone_select.js.coffee
app/assets/javascripts/milestone_select.js.coffee
+1
-1
app/assets/javascripts/raven_config.js.coffee
app/assets/javascripts/raven_config.js.coffee
+44
-0
app/assets/javascripts/users_select.js.coffee
app/assets/javascripts/users_select.js.coffee
+1
-1
app/assets/stylesheets/framework/dropdowns.scss
app/assets/stylesheets/framework/dropdowns.scss
+128
-2
app/assets/stylesheets/framework/markdown_area.scss
app/assets/stylesheets/framework/markdown_area.scss
+9
-0
app/assets/stylesheets/framework/variables.scss
app/assets/stylesheets/framework/variables.scss
+5
-0
app/assets/stylesheets/pages/detail_page.scss
app/assets/stylesheets/pages/detail_page.scss
+6
-0
app/assets/stylesheets/pages/diff.scss
app/assets/stylesheets/pages/diff.scss
+3
-2
app/assets/stylesheets/pages/help.scss
app/assets/stylesheets/pages/help.scss
+2
-0
app/assets/stylesheets/pages/issuable.scss
app/assets/stylesheets/pages/issuable.scss
+3
-2
app/assets/stylesheets/pages/note_form.scss
app/assets/stylesheets/pages/note_form.scss
+0
-12
app/assets/stylesheets/pages/notes.scss
app/assets/stylesheets/pages/notes.scss
+4
-17
app/controllers/projects/issues_controller.rb
app/controllers/projects/issues_controller.rb
+4
-3
app/controllers/projects/merge_requests_controller.rb
app/controllers/projects/merge_requests_controller.rb
+3
-2
app/finders/issuable_finder.rb
app/finders/issuable_finder.rb
+40
-1
app/helpers/application_helper.rb
app/helpers/application_helper.rb
+8
-1
app/helpers/issuables_helper.rb
app/helpers/issuables_helper.rb
+19
-0
app/helpers/issues_helper.rb
app/helpers/issues_helper.rb
+13
-1
app/helpers/sorting_helper.rb
app/helpers/sorting_helper.rb
+18
-0
app/models/issue.rb
app/models/issue.rb
+27
-0
app/models/label.rb
app/models/label.rb
+4
-0
app/views/projects/_activity.html.haml
app/views/projects/_activity.html.haml
+4
-1
app/views/projects/issues/_issue.html.haml
app/views/projects/issues/_issue.html.haml
+5
-0
app/views/projects/merge_requests/show/_mr_title.html.haml
app/views/projects/merge_requests/show/_mr_title.html.haml
+2
-2
app/views/shared/_label_row.html.haml
app/views/shared/_label_row.html.haml
+1
-1
app/views/shared/_labels_row.html.haml
app/views/shared/_labels_row.html.haml
+3
-0
app/views/shared/_sort_dropdown.html.haml
app/views/shared/_sort_dropdown.html.haml
+5
-0
app/views/shared/issuable/_filter.html.haml
app/views/shared/issuable/_filter.html.haml
+5
-3
app/views/shared/issuable/_label_dropdown.html.haml
app/views/shared/issuable/_label_dropdown.html.haml
+5
-3
app/views/shared/issuable/_nav.html.haml
app/views/shared/issuable/_nav.html.haml
+5
-5
app/views/shared/issuable/_sidebar.html.haml
app/views/shared/issuable/_sidebar.html.haml
+34
-5
db/migrate/20160310124959_add_due_date_to_issues.rb
db/migrate/20160310124959_add_due_date_to_issues.rb
+6
-0
db/schema.rb
db/schema.rb
+2
-0
features/project/issues/filter_labels.feature
features/project/issues/filter_labels.feature
+1
-0
features/steps/project/issues/filter_labels.rb
features/steps/project/issues/filter_labels.rb
+4
-0
lib/api/internal.rb
lib/api/internal.rb
+1
-1
lib/gitlab/gon_helper.rb
lib/gitlab/gon_helper.rb
+1
-0
lib/gitlab/metrics.rb
lib/gitlab/metrics.rb
+9
-0
spec/features/issues/filter_by_labels_spec.rb
spec/features/issues/filter_by_labels_spec.rb
+172
-0
spec/features/issues/filter_issues_spec.rb
spec/features/issues/filter_issues_spec.rb
+8
-1
spec/features/issues_spec.rb
spec/features/issues_spec.rb
+92
-6
spec/features/merge_requests/filter_by_milestone_spec.rb
spec/features/merge_requests/filter_by_milestone_spec.rb
+6
-0
spec/finders/issues_finder_spec.rb
spec/finders/issues_finder_spec.rb
+16
-0
spec/lib/gitlab/metrics_spec.rb
spec/lib/gitlab/metrics_spec.rb
+24
-0
vendor/assets/javascripts/raven.js
vendor/assets/javascripts/raven.js
+2435
-0
No files found.
CHANGELOG
View file @
7e0ef892
...
@@ -19,6 +19,7 @@ v 8.7.0 (unreleased)
...
@@ -19,6 +19,7 @@ v 8.7.0 (unreleased)
- Load award emoji images separately unless opening the full picker. Saves several hundred KBs of data for most pages. (Connor Shea)
- 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)
- Do not include award_emojis in issue and merge_request comment_count !3610 (Lucas Charles)
- Restrict user profiles when public visibility level is restricted.
- 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).
- 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)
- 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
- Add setting for customizing the list of trusted proxies !3524
...
@@ -86,6 +87,10 @@ v 8.7.0 (unreleased)
...
@@ -86,6 +87,10 @@ v 8.7.0 (unreleased)
- Fix repository cache invalidation issue when project is recreated with an empty repo (Stan Hu)
- 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)
- Fix: Allow empty recipients list for builds emails service when pushed is added (Frank Groeneveld)
- Improved markdown forms
- 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
- Delete tags using Rugged for performance reasons (Robert Schilling)
- Delete tags using Rugged for performance reasons (Robert Schilling)
- Add Slack notifications when Wiki is edited (Sebastian Klier)
- Add Slack notifications when Wiki is edited (Sebastian Klier)
- Diffs load at the correct point when linking from from number
- Diffs load at the correct point when linking from from number
...
@@ -107,7 +112,7 @@ v 8.7.0 (unreleased)
...
@@ -107,7 +112,7 @@ v 8.7.0 (unreleased)
- Add RAW build trace output and button on build page
- Add RAW build trace output and button on build page
- Add incremental build trace update into CI API
- Add incremental build trace update into CI API
v 8.6.7
(unreleased)
v 8.6.7
- Fix persistent XSS vulnerability in `commit_person_link` helper
- Fix persistent XSS vulnerability in `commit_person_link` helper
- Fix persistent XSS vulnerability in Label and Milestone dropdowns
- Fix persistent XSS vulnerability in Label and Milestone dropdowns
- Fix vulnerability that made it possible to enumerate private projects belonging to group
- Fix vulnerability that made it possible to enumerate private projects belonging to group
...
@@ -117,7 +122,6 @@ v 8.6.6
...
@@ -117,7 +122,6 @@ v 8.6.6
- Fix error on language detection when repository has no HEAD (e.g., master branch) (Jeroen Bobbeldijk). !3654
- 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 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)
- 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
- Issuable header is consistent between issues and merge requests
- Improved spacing in issuable header on mobile
- Improved spacing in issuable header on mobile
...
@@ -249,6 +253,9 @@ v 8.6.0
...
@@ -249,6 +253,9 @@ v 8.6.0
- Trigger a todo for mentions on commits page
- Trigger a todo for mentions on commits page
- Let project owners and admins soft delete issues and merge requests
- 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
v 8.5.10
- Fix a 2FA authentication spoofing vulnerability.
- Fix a 2FA authentication spoofing vulnerability.
...
@@ -396,6 +403,9 @@ v 8.5.0
...
@@ -396,6 +403,9 @@ v 8.5.0
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Show label row when filtering issues or merge requests by label (Nuttanart Pornprasitsakul)
- Add Todos
- Add Todos
v 8.4.9
- Fix persistent XSS vulnerability in `commit_person_link` helper
v 8.4.8
v 8.4.8
- Fix a 2FA authentication spoofing vulnerability.
- Fix a 2FA authentication spoofing vulnerability.
...
@@ -518,6 +528,9 @@ v 8.4.0
...
@@ -518,6 +528,9 @@ v 8.4.0
- Add IP check against DNSBLs at account sign-up
- Add IP check against DNSBLs at account sign-up
- Added cache:key to .gitlab-ci.yml allowing to fine tune the caching
- 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
v 8.3.7
- Fix a 2FA authentication spoofing vulnerability.
- Fix a 2FA authentication spoofing vulnerability.
...
...
CONTRIBUTING.md
View file @
7e0ef892
...
@@ -323,6 +323,7 @@ request is as follows:
...
@@ -323,6 +323,7 @@ request is as follows:
[
shell command guidelines
](
doc/development/shell_commands.md
)
[
shell command guidelines
](
doc/development/shell_commands.md
)
1.
If your code creates new files on disk please read the
1.
If your code creates new files on disk please read the
[
shared files guidelines
](
doc/development/shared_files.md
)
.
[
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
**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
the 7th day of the month. This is the best time to submit an MR and get
...
...
app/assets/javascripts/application.js.coffee
View file @
7e0ef892
...
@@ -55,6 +55,7 @@
...
@@ -55,6 +55,7 @@
#= require_tree .
#= require_tree .
#= require fuzzaldrin-plus
#= require fuzzaldrin-plus
#= require cropper
#= require cropper
#= require raven
window
.
slugify
=
(
text
)
->
window
.
slugify
=
(
text
)
->
text
.
replace
(
/[^-a-zA-Z0-9]+/g
,
'_'
).
toLowerCase
()
text
.
replace
(
/[^-a-zA-Z0-9]+/g
,
'_'
).
toLowerCase
()
...
...
app/assets/javascripts/dispatcher.js.coffee
View file @
7e0ef892
...
@@ -17,6 +17,7 @@ class Dispatcher
...
@@ -17,6 +17,7 @@ class Dispatcher
switch
page
switch
page
when
'projects:issues:index'
when
'projects:issues:index'
Issues
.
init
()
Issues
.
init
()
Issuable
.
init
()
shortcut_handler
=
new
ShortcutsNavigation
()
shortcut_handler
=
new
ShortcutsNavigation
()
when
'projects:issues:show'
when
'projects:issues:show'
new
Issue
()
new
Issue
()
...
@@ -57,7 +58,7 @@ class Dispatcher
...
@@ -57,7 +58,7 @@ class Dispatcher
new
ZenMode
()
new
ZenMode
()
when
'projects:merge_requests:index'
when
'projects:merge_requests:index'
shortcut_handler
=
new
ShortcutsNavigation
()
shortcut_handler
=
new
ShortcutsNavigation
()
MergeRequests
.
init
()
Issuable
.
init
()
when
'dashboard:activity'
when
'dashboard:activity'
new
Activities
()
new
Activities
()
when
'dashboard:projects:starred'
when
'dashboard:projects:starred'
...
...
app/assets/javascripts/due_date_select.js.coffee
0 → 100644
View file @
7e0ef892
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
()
app/assets/javascripts/gl_dropdown.js.coffee
View file @
7e0ef892
...
@@ -389,13 +389,13 @@ class GitLabDropdown
...
@@ -389,13 +389,13 @@ class GitLabDropdown
else
else
selectedObject
selectedObject
else
else
if
!
value
?
if
not
@
options
.
multiSelect
or
el
.
hasClass
(
'dropdown-clear-active'
)
field
.
remove
()
if
not
@
options
.
multiSelect
@
dropdown
.
find
(
".
#{
ACTIVE_CLASS
}
"
).
removeClass
ACTIVE_CLASS
@
dropdown
.
find
(
".
#{
ACTIVE_CLASS
}
"
).
removeClass
ACTIVE_CLASS
@
dropdown
.
parent
().
find
(
"input[name='
#{
fieldName
}
']"
).
remove
()
@
dropdown
.
parent
().
find
(
"input[name='
#{
fieldName
}
']"
).
remove
()
if
!
value
?
field
.
remove
()
# Toggle active class for the tick mark
# Toggle active class for the tick mark
el
.
addClass
ACTIVE_CLASS
el
.
addClass
ACTIVE_CLASS
...
...
app/assets/javascripts/issuable.js.coffee
0 → 100644
View file @
7e0ef892
@
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
app/assets/javascripts/issuable_context.js.coffee
View file @
7e0ef892
...
@@ -33,7 +33,6 @@ class @IssuableContext
...
@@ -33,7 +33,6 @@ class @IssuableContext
$block
.
find
(
'.dropdown-menu-toggle'
).
trigger
'click'
$block
.
find
(
'.dropdown-menu-toggle'
).
trigger
'click'
,
0
,
0
$
(
".right-sidebar"
).
niceScroll
()
$
(
".right-sidebar"
).
niceScroll
()
initParticipants
:
->
initParticipants
:
->
...
...
app/assets/javascripts/issues.js.coffee
View file @
7e0ef892
@
Issues
=
@
Issues
=
init
:
->
init
:
->
Issues
.
initSearch
()
Issues
.
created
=
true
Issues
.
initChecks
()
Issues
.
initChecks
()
$
(
"body"
).
on
"ajax:success"
,
".close_issue, .reopen_issue"
,
->
$
(
"body"
).
on
"ajax:success"
,
".close_issue, .reopen_issue"
,
->
...
@@ -15,10 +15,6 @@
...
@@ -15,10 +15,6 @@
else
else
$
(
this
).
html
totalIssues
-
1
$
(
this
).
html
totalIssues
-
1
reload
:
->
Issues
.
initChecks
()
$
(
'#filter_issue_search'
).
val
(
$
(
'#issue_search'
).
val
())
initChecks
:
->
initChecks
:
->
$
(
".check_all_issues"
).
click
->
$
(
".check_all_issues"
).
click
->
$
(
".selected_issue"
).
prop
(
"checked"
,
@
checked
)
$
(
".selected_issue"
).
prop
(
"checked"
,
@
checked
)
...
@@ -26,51 +22,6 @@
...
@@ -26,51 +22,6 @@
$
(
".selected_issue"
).
bind
"change"
,
Issues
.
checkChanged
$
(
".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
:
->
checkChanged
:
->
checked_issues
=
$
(
".selected_issue:checked"
)
checked_issues
=
$
(
".selected_issue:checked"
)
if
checked_issues
.
length
>
0
if
checked_issues
.
length
>
0
...
...
app/assets/javascripts/labels_select.js.coffee
View file @
7e0ef892
...
@@ -6,7 +6,7 @@ class @LabelsSelect
...
@@ -6,7 +6,7 @@ class @LabelsSelect
labelUrl
=
$dropdown
.
data
(
'labels'
)
labelUrl
=
$dropdown
.
data
(
'labels'
)
issueUpdateURL
=
$dropdown
.
data
(
'issueUpdate'
)
issueUpdateURL
=
$dropdown
.
data
(
'issueUpdate'
)
selectedLabel
=
$dropdown
.
data
(
'selected'
)
selectedLabel
=
$dropdown
.
data
(
'selected'
)
if
selectedLabel
?
if
selectedLabel
?
and
not
$dropdown
.
hasClass
'js-multiselect'
selectedLabel
=
selectedLabel
.
split
(
','
)
selectedLabel
=
selectedLabel
.
split
(
','
)
newLabelField
=
$
(
'#new_label_name'
)
newLabelField
=
$
(
'#new_label_name'
)
newColorField
=
$
(
'#new_label_color'
)
newColorField
=
$
(
'#new_label_color'
)
...
@@ -16,6 +16,7 @@ class @LabelsSelect
...
@@ -16,6 +16,7 @@ class @LabelsSelect
abilityName
=
$dropdown
.
data
(
'ability-name'
)
abilityName
=
$dropdown
.
data
(
'ability-name'
)
$selectbox
=
$dropdown
.
closest
(
'.selectbox'
)
$selectbox
=
$dropdown
.
closest
(
'.selectbox'
)
$block
=
$selectbox
.
closest
(
'.block'
)
$block
=
$selectbox
.
closest
(
'.block'
)
$form
=
$dropdown
.
closest
(
'form'
)
$sidebarCollapsedValue
=
$block
.
find
(
'.sidebar-collapsed-icon span'
)
$sidebarCollapsedValue
=
$block
.
find
(
'.sidebar-collapsed-icon span'
)
$value
=
$block
.
find
(
'.value'
)
$value
=
$block
.
find
(
'.value'
)
$newLabelError
=
$
(
'.js-label-error'
)
$newLabelError
=
$
(
'.js-label-error'
)
...
@@ -151,7 +152,7 @@ class @LabelsSelect
...
@@ -151,7 +152,7 @@ class @LabelsSelect
.
find
(
'a'
)
.
find
(
'a'
)
.
each
((
i
)
->
.
each
((
i
)
->
setTimeout
(
=>
setTimeout
(
=>
gl
A
nimate
(
$
(
@
),
'pulse'
)
gl
.
animate
.
a
nimate
(
$
(
@
),
'pulse'
)
,
200
*
i
,
200
*
i
)
)
)
)
...
@@ -180,16 +181,21 @@ class @LabelsSelect
...
@@ -180,16 +181,21 @@ class @LabelsSelect
callback
data
callback
data
renderRow
:
(
label
)
->
renderRow
:
(
label
)
->
selectedClass
=
''
removesAll
=
label
.
id
is
0
or
not
label
.
id
?
if
$selectbox
.
find
(
"input[type='hidden']
\
[name='
#{
$dropdown
.
data
(
'field-name'
)
}
']
\
selectedClass
=
[]
[value='
#{
label
.
id
}
']"
).
length
if
$form
.
find
(
"input[type='hidden']
\
selectedClass
=
'is-active'
[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
""
color
=
if
label
.
color
?
then
"<span class='dropdown-label-box' style='background-color:
#{
label
.
color
}
'></span>"
else
""
"<li>
"<li>
<a href='#' class='
#{
selectedClass
}
'>
<a href='#' class='
#{
selectedClass
.
join
(
' '
)
}
'>
#{
color
}
#{
color
}
#{
_
.
escape
(
label
.
title
)
}
#{
_
.
escape
(
label
.
title
)
}
</a>
</a>
...
@@ -199,37 +205,56 @@ class @LabelsSelect
...
@@ -199,37 +205,56 @@ class @LabelsSelect
fields
:
[
'title'
]
fields
:
[
'title'
]
selectable
:
true
selectable
:
true
toggleLabel
:
(
selected
)
->
toggleLabel
:
(
selected
,
el
)
->
selected_labels
=
$
(
'.js-label-select'
).
siblings
(
'.dropdown-menu-labels'
).
find
(
'.is-active'
)
if
selected
and
selected
.
title
?
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
else
defaultLabel
defaultLabel
fieldName
:
$dropdown
.
data
(
'field-name'
)
fieldName
:
$dropdown
.
data
(
'field-name'
)
id
:
(
label
)
->
id
:
(
label
)
->
if
label
.
isAny
?
if
$dropdown
.
hasClass
(
"js-filter-submit"
)
and
not
label
.
isAny
?
''
else
if
$dropdown
.
hasClass
"js-filter-submit"
label
.
title
label
.
title
else
else
label
.
id
label
.
id
hidden
:
->
hidden
:
->
page
=
$
(
'body'
).
data
'page'
isIssueIndex
=
page
is
'projects:issues:index'
isMRIndex
=
page
is
'projects:merge_requests:index'
$selectbox
.
hide
()
$selectbox
.
hide
()
# display:block overrides the hide-collapse rule
# display:block overrides the hide-collapse rule
$value
.
removeAttr
(
'style'
)
$value
.
removeAttr
(
'style'
)
if
$dropdown
.
hasClass
'js-multiselect'
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'
multiSelect
:
$dropdown
.
hasClass
'js-multiselect'
clicked
:
(
label
)
->
clicked
:
(
label
)
->
page
=
$
(
'body'
).
data
'page'
page
=
$
(
'body'
).
data
'page'
isIssueIndex
=
page
is
'projects:issues:index'
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
)
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
and
(
isIssueIndex
or
isMRIndex
)
selectedLabel
=
label
.
title
if
not
$dropdown
.
hasClass
'js-multiselect'
selectedLabel
=
label
.
title
Issues
.
filterResults
$dropdown
.
closest
(
'form'
)
Issuable
.
filterResults
$dropdown
.
closest
(
'form'
)
else
if
$dropdown
.
hasClass
'js-filter-submit'
else
if
$dropdown
.
hasClass
'js-filter-submit'
$dropdown
.
closest
(
'form'
).
submit
()
$dropdown
.
closest
(
'form'
).
submit
()
else
else
...
...
app/assets/javascripts/lib/animate.js.coffee
View file @
7e0ef892
((
w
)
->
((
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
$el
.
removeClass
()
.
removeClass
(
animation
+
' animated'
)
.
addClass
(
animation
+
' animated'
)
.
addClass
(
animation
+
' animated'
)
.
one
'webkitAnimationEnd mozAnimationEnd MSAnimationEnd oanimationend animationend'
,
->
.
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
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
)
window
\ No newline at end of file
app/assets/javascripts/lib/url_utility.js.coffee
View file @
7e0ef892
...
@@ -3,16 +3,20 @@
...
@@ -3,16 +3,20 @@
w
.
gl
?=
{}
w
.
gl
?=
{}
w
.
gl
.
utils
?=
{}
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
))
sPageURL
=
decodeURIComponent
(
window
.
location
.
search
.
substring
(
1
))
sURLVariables
=
sPageURL
.
split
(
'&'
)
sURLVariables
=
sPageURL
.
split
(
'&'
)
sParameterName
=
undefined
sParameterName
=
undefined
values
=
[]
i
=
0
i
=
0
while
i
<
sURLVariables
.
length
while
i
<
sURLVariables
.
length
sParameterName
=
sURLVariables
[
i
].
split
(
'='
)
sParameterName
=
sURLVariables
[
i
].
split
(
'='
)
if
sParameterName
[
0
]
is
sParam
if
sParameterName
[
0
]
is
sParam
return
if
sParameterName
[
1
]
is
undefined
then
true
else
sParameterName
[
1
]
values
.
push
(
sParameterName
[
1
])
i
++
i
++
values
# #
# #
# @param {Object} params - url keys and value to merge
# @param {Object} params - url keys and value to merge
...
@@ -28,4 +32,12 @@
...
@@ -28,4 +32,12 @@
newUrl
=
"
#{
newUrl
}#{
(
if
newUrl
.
indexOf
(
'?'
)
>
0
then
'&'
else
'?'
)
}#{
paramName
}
=
#{
paramValue
}
"
newUrl
=
"
#{
newUrl
}#{
(
if
newUrl
.
indexOf
(
'?'
)
>
0
then
'&'
else
'?'
)
}#{
paramName
}
=
#{
paramValue
}
"
newUrl
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
)
window
app/assets/javascripts/merge_requests.js.coffee
deleted
100644 → 0
View file @
a5290ac2
#
# * 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
())
app/assets/javascripts/milestone_select.js.coffee
View file @
7e0ef892
...
@@ -97,7 +97,7 @@ class @MilestoneSelect
...
@@ -97,7 +97,7 @@ class @MilestoneSelect
selectedMilestone
=
selected
.
name
selectedMilestone
=
selected
.
name
else
else
selectedMilestone
=
''
selectedMilestone
=
''
Issu
es
.
filterResults
$dropdown
.
closest
(
'form'
)
Issu
able
.
filterResults
$dropdown
.
closest
(
'form'
)
else
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
else
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
$dropdown
.
closest
(
'form'
).
submit
()
$dropdown
.
closest
(
'form'
).
submit
()
else
else
...
...
app/assets/javascripts/raven_config.js.coffee
0 → 100644
View file @
7e0ef892
@
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
()
app/assets/javascripts/users_select.js.coffee
View file @
7e0ef892
...
@@ -158,7 +158,7 @@ class @UsersSelect
...
@@ -158,7 +158,7 @@ class @UsersSelect
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
and
(
isIssueIndex
or
isMRIndex
)
if
$dropdown
.
hasClass
(
'js-filter-submit'
)
and
(
isIssueIndex
or
isMRIndex
)
selectedId
=
user
.
id
selectedId
=
user
.
id
Issu
es
.
filterResults
$dropdown
.
closest
(
'form'
)
Issu
able
.
filterResults
$dropdown
.
closest
(
'form'
)
else
if
$dropdown
.
hasClass
'js-filter-submit'
else
if
$dropdown
.
hasClass
'js-filter-submit'
$dropdown
.
closest
(
'form'
).
submit
()
$dropdown
.
closest
(
'form'
).
submit
()
else
else
...
...
app/assets/stylesheets/framework/dropdowns.scss
View file @
7e0ef892
...
@@ -248,7 +248,7 @@
...
@@ -248,7 +248,7 @@
.dropdown-title
{
.dropdown-title
{
position
:
relative
;
position
:
relative
;
padding
:
0
25px
1
5
px
;
padding
:
0
25px
1
0
px
;
margin
:
0
10px
10px
;
margin
:
0
10px
10px
;
font-weight
:
600
;
font-weight
:
600
;
line-height
:
1
;
line-height
:
1
;
...
@@ -278,7 +278,7 @@
...
@@ -278,7 +278,7 @@
right
:
5px
;
right
:
5px
;
width
:
20px
;
width
:
20px
;
height
:
20px
;
height
:
20px
;
top
:
-
1
px
;
top
:
-
3
px
;
}
}
.dropdown-menu-back
{
.dropdown-menu-back
{
...
@@ -358,6 +358,13 @@
...
@@ -358,6 +358,13 @@
border-top
:
1px
solid
$dropdown-divider-color
;
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
{
.dropdown-footer-list
{
font-size
:
14px
;
font-size
:
14px
;
...
@@ -395,3 +402,122 @@
...
@@ -395,3 +402,122 @@
height
:
15px
;
height
:
15px
;
border-radius
:
$border-radius-base
;
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
;
}
}
app/assets/stylesheets/framework/markdown_area.scss
View file @
7e0ef892
...
@@ -90,3 +90,12 @@
...
@@ -90,3 +90,12 @@
box-shadow
:
none
;
box-shadow
:
none
;
width
:
100%
;
width
:
100%
;
}
}
.md
{
&
.md-preview-holder
{
code
{
white-space
:
pre-wrap
;
word-break
:
break-all
;
}
}
}
app/assets/stylesheets/framework/variables.scss
View file @
7e0ef892
...
@@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5;
...
@@ -241,3 +241,8 @@ $note-form-border-color: #e5e5e5;
$note-toolbar-color
:
#959494
;
$note-toolbar-color
:
#959494
;
$zen-control-hover-color
:
#111
;
$zen-control-hover-color
:
#111
;
$calendar-header-color
:
#b8b8b8
;
$calendar-hover-bg
:
#ecf3fe
;
$calendar-border-color
:
rgba
(
#000
,
.1
);
$calendar-unselectable-bg
:
#faf9f9
;
app/assets/stylesheets/pages/detail_page.scss
View file @
7e0ef892
...
@@ -36,4 +36,10 @@
...
@@ -36,4 +36,10 @@
}
}
}
}
}
}
.wiki
{
code
{
white-space
:
pre-wrap
;
}
}
}
}
app/assets/stylesheets/pages/diff.scss
View file @
7e0ef892
...
@@ -34,6 +34,7 @@
...
@@ -34,6 +34,7 @@
background
:
#fff
;
background
:
#fff
;
color
:
#333
;
color
:
#333
;
border-radius
:
0
0
3px
3px
;
border-radius
:
0
0
3px
3px
;
-webkit-overflow-scrolling
:
auto
;
.unfold
{
.unfold
{
cursor
:
pointer
;
cursor
:
pointer
;
...
@@ -86,7 +87,7 @@
...
@@ -86,7 +87,7 @@
}
}
span
{
span
{
white-space
:
pre
;
white-space
:
pre
-wrap
;
}
}
}
}
}
}
...
@@ -335,7 +336,7 @@
...
@@ -335,7 +336,7 @@
}
}
.diff-file
.line_content
{
.diff-file
.line_content
{
white-space
:
pre
;
white-space
:
pre
-wrap
;
}
}
.diff-wrap-lines
.line_content
{
.diff-wrap-lines
.line_content
{
...
...
app/assets/stylesheets/pages/help.scss
View file @
7e0ef892
...
@@ -59,8 +59,10 @@
...
@@ -59,8 +59,10 @@
position
:
relative
;
position
:
relative
;
overflow-y
:
auto
;
overflow-y
:
auto
;
padding
:
15px
;
padding
:
15px
;
.form-actions
{
.form-actions
{
margin
:
-
$gl-padding
+
1
;
margin
:
-
$gl-padding
+
1
;
margin-top
:
15px
;
}
}
}
}
...
...
app/assets/stylesheets/pages/issuable.scss
View file @
7e0ef892
...
@@ -128,6 +128,7 @@
...
@@ -128,6 +128,7 @@
top
:
58px
;
top
:
58px
;
bottom
:
0
;
bottom
:
0
;
right
:
0
;
right
:
0
;
z-index
:
10
;
transition
:
width
.3s
;
transition
:
width
.3s
;
background
:
$gray-light
;
background
:
$gray-light
;
padding
:
10px
20px
;
padding
:
10px
20px
;
...
@@ -241,7 +242,7 @@
...
@@ -241,7 +242,7 @@
}
}
}
}
.
btn
{
.
issuable-pager
{
background
:
$gray-normal
;
background
:
$gray-normal
;
border
:
1px
solid
$border-gray-normal
;
border
:
1px
solid
$border-gray-normal
;
&
:hover
{
&
:hover
{
...
@@ -254,7 +255,7 @@
...
@@ -254,7 +255,7 @@
}
}
}
}
a
:not
(
.
btn
)
{
a
:not
(
.
issuable-pager
)
{
&
:hover
{
&
:hover
{
color
:
$md-link-color
;
color
:
$md-link-color
;
text-decoration
:
none
;
text-decoration
:
none
;
...
...
app/assets/stylesheets/pages/note_form.scss
View file @
7e0ef892
...
@@ -84,18 +84,6 @@
...
@@ -84,18 +84,6 @@
border-color
:
$gl-success
;
border-color
:
$gl-success
;
}
}
}
}
p
{
code
{
white-space
:
normal
;
}
pre
{
code
{
white-space
:
pre
;
}
}
}
}
}
}
}
...
...
app/assets/stylesheets/pages/notes.scss
View file @
7e0ef892
...
@@ -81,16 +81,8 @@ ul.notes {
...
@@ -81,16 +81,8 @@ ul.notes {
@include
md-typography
;
@include
md-typography
;
// On diffs code should wrap nicely and not overflow
// On diffs code should wrap nicely and not overflow
p
{
code
{
code
{
white-space
:
pre-wrap
;
white-space
:
normal
;
}
pre
{
code
{
white-space
:
pre
;
}
}
}
}
// Reset ul style types since we're nested inside a ul already
// Reset ul style types since we're nested inside a ul already
...
@@ -137,7 +129,7 @@ ul.notes {
...
@@ -137,7 +129,7 @@ ul.notes {
margin-right
:
10px
;
margin-right
:
10px
;
}
}
.line_content
{
.line_content
{
white-space
:
pre
;
white-space
:
pre
-wrap
;
}
}
}
}
...
@@ -171,11 +163,6 @@ ul.notes {
...
@@ -171,11 +163,6 @@ ul.notes {
&
.parallel
{
&
.parallel
{
border-width
:
1px
;
border-width
:
1px
;
.code
,
code
{
white-space
:
pre-wrap
;
}
}
}
.notes
{
.notes
{
...
@@ -308,7 +295,7 @@ ul.notes {
...
@@ -308,7 +295,7 @@ ul.notes {
padding
:
4px
;
padding
:
4px
;
font-size
:
16px
;
font-size
:
16px
;
color
:
$gl-link-color
;
color
:
$gl-link-color
;
margin-left
:
-
60
px
;
margin-left
:
-
56
px
;
position
:
absolute
;
position
:
absolute
;
z-index
:
10
;
z-index
:
10
;
width
:
32px
;
width
:
32px
;
...
...
app/controllers/projects/issues_controller.rb
View file @
7e0ef892
...
@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -33,14 +33,15 @@ class Projects::IssuesController < Projects::ApplicationController
end
end
@issues
=
@issues
.
page
(
params
[
:page
])
@issues
=
@issues
.
page
(
params
[
:page
])
@label
=
@project
.
labels
.
find_by
(
title:
params
[
:label_name
])
@label
s
=
@project
.
labels
.
where
(
title:
params
[
:label_name
])
respond_to
do
|
format
|
respond_to
do
|
format
|
format
.
html
format
.
html
format
.
atom
{
render
layout:
false
}
format
.
atom
{
render
layout:
false
}
format
.
json
do
format
.
json
do
render
json:
{
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
end
end
...
@@ -191,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
...
@@ -191,7 +192,7 @@ class Projects::IssuesController < Projects::ApplicationController
def
issue_params
def
issue_params
params
.
require
(
:issue
).
permit
(
params
.
require
(
:issue
).
permit
(
:title
,
:assignee_id
,
:position
,
:description
,
:confidential
,
: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
end
...
...
app/controllers/projects/merge_requests_controller.rb
View file @
7e0ef892
...
@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
...
@@ -38,13 +38,14 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@merge_requests
=
@merge_requests
.
page
(
params
[
:page
])
@merge_requests
=
@merge_requests
.
page
(
params
[
:page
])
@merge_requests
=
@merge_requests
.
preload
(
:target_project
)
@merge_requests
=
@merge_requests
.
preload
(
:target_project
)
@label
=
@project
.
labels
.
find_by
(
title:
params
[
:label_name
])
@label
s
=
@project
.
labels
.
where
(
title:
params
[
:label_name
])
respond_to
do
|
format
|
respond_to
do
|
format
|
format
.
html
format
.
html
format
.
json
do
format
.
json
do
render
json:
{
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
end
end
...
...
app/finders/issuable_finder.rb
View file @
7e0ef892
...
@@ -39,6 +39,7 @@ class IssuableFinder
...
@@ -39,6 +39,7 @@ class IssuableFinder
items
=
by_assignee
(
items
)
items
=
by_assignee
(
items
)
items
=
by_author
(
items
)
items
=
by_author
(
items
)
items
=
by_label
(
items
)
items
=
by_label
(
items
)
items
=
by_due_date
(
items
)
sort
(
items
)
sort
(
items
)
end
end
...
@@ -117,7 +118,7 @@ class IssuableFinder
...
@@ -117,7 +118,7 @@ class IssuableFinder
end
end
def
filter_by_no_label?
def
filter_by_no_label?
labels?
&&
params
[
:label_name
]
==
Label
::
None
.
title
labels?
&&
params
[
:label_name
]
.
include?
(
Label
::
None
.
title
)
end
end
def
labels
def
labels
...
@@ -278,9 +279,47 @@ class IssuableFinder
...
@@ -278,9 +279,47 @@ class IssuableFinder
end
end
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
items
end
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
def
label_names
params
[
:label_name
].
split
(
','
)
params
[
:label_name
].
split
(
','
)
end
end
...
...
app/helpers/application_helper.rb
View file @
7e0ef892
...
@@ -254,11 +254,11 @@ module ApplicationHelper
...
@@ -254,11 +254,11 @@ module ApplicationHelper
def
page_filter_path
(
options
=
{})
def
page_filter_path
(
options
=
{})
without
=
options
.
delete
(
:without
)
without
=
options
.
delete
(
:without
)
add_label
=
options
.
delete
(
:label
)
exist_opts
=
{
exist_opts
=
{
state:
params
[
:state
],
state:
params
[
:state
],
scope:
params
[
:scope
],
scope:
params
[
:scope
],
label_name:
params
[
:label_name
],
milestone_title:
params
[
:milestone_title
],
milestone_title:
params
[
:milestone_title
],
assignee_id:
params
[
:assignee_id
],
assignee_id:
params
[
:assignee_id
],
author_id:
params
[
:author_id
],
author_id:
params
[
:author_id
],
...
@@ -275,6 +275,13 @@ module ApplicationHelper
...
@@ -275,6 +275,13 @@ module ApplicationHelper
path
=
request
.
path
path
=
request
.
path
path
<<
"?
#{
options
.
to_param
}
"
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
path
end
end
...
...
app/helpers/issuables_helper.rb
View file @
7e0ef892
...
@@ -16,6 +16,25 @@ module IssuablesHelper
...
@@ -16,6 +16,25 @@ module IssuablesHelper
base_issuable_scope
(
issuable
).
where
(
'iid > ?'
,
issuable
.
iid
).
last
base_issuable_scope
(
issuable
).
where
(
'iid > ?'
,
issuable
.
iid
).
last
end
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
)
def
issuable_json_path
(
issuable
)
project
=
issuable
.
project
project
=
issuable
.
project
...
...
app/helpers/issues_helper.rb
View file @
7e0ef892
...
@@ -131,7 +131,7 @@ module IssuesHelper
...
@@ -131,7 +131,7 @@ module IssuesHelper
class:
"icon emoji-icon emoji-
#{
unicode
}
"
,
class:
"icon emoji-icon emoji-
#{
unicode
}
"
,
title:
name
,
title:
name
,
data:
data
data:
data
else
else
# Emoji icons displayed separately, used for the awards already given
# Emoji icons displayed separately, used for the awards already given
# to an issue or merge request.
# to an issue or merge request.
content_tag
:img
,
""
,
content_tag
:img
,
""
,
...
@@ -172,6 +172,18 @@ module IssuesHelper
...
@@ -172,6 +172,18 @@ module IssuesHelper
end
.
to_h
end
.
to_h
end
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
# Required for Banzai::Filter::IssueReferenceFilter
module_function
:url_for_issue
module_function
:url_for_issue
end
end
app/helpers/sorting_helper.rb
View file @
7e0ef892
...
@@ -8,6 +8,8 @@ module SortingHelper
...
@@ -8,6 +8,8 @@ module SortingHelper
sort_value_oldest_created
=>
sort_title_oldest_created
,
sort_value_oldest_created
=>
sort_title_oldest_created
,
sort_value_milestone_soon
=>
sort_title_milestone_soon
,
sort_value_milestone_soon
=>
sort_title_milestone_soon
,
sort_value_milestone_later
=>
sort_title_milestone_later
,
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_largest_repo
=>
sort_title_largest_repo
,
sort_value_recently_signin
=>
sort_title_recently_signin
,
sort_value_recently_signin
=>
sort_title_recently_signin
,
sort_value_oldest_signin
=>
sort_title_oldest_signin
,
sort_value_oldest_signin
=>
sort_title_oldest_signin
,
...
@@ -50,6 +52,14 @@ module SortingHelper
...
@@ -50,6 +52,14 @@ module SortingHelper
'Milestone due later'
'Milestone due later'
end
end
def
sort_title_due_date_soon
'Due soon'
end
def
sort_title_due_date_later
'Due later'
end
def
sort_title_name
def
sort_title_name
'Name'
'Name'
end
end
...
@@ -98,6 +108,14 @@ module SortingHelper
...
@@ -98,6 +108,14 @@ module SortingHelper
'milestone_due_desc'
'milestone_due_desc'
end
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
def
sort_value_name
'name_asc'
'name_asc'
end
end
...
...
app/models/issue.rb
View file @
7e0ef892
...
@@ -28,6 +28,13 @@ class Issue < ActiveRecord::Base
...
@@ -28,6 +28,13 @@ class Issue < ActiveRecord::Base
include
Sortable
include
Sortable
include
Taskable
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
ActsAsTaggableOn
.
strict_case_match
=
true
belongs_to
:project
belongs_to
:project
...
@@ -39,6 +46,13 @@ class Issue < ActiveRecord::Base
...
@@ -39,6 +46,13 @@ class Issue < ActiveRecord::Base
scope
:open_for
,
->
(
user
)
{
opened
.
assigned_to
(
user
)
}
scope
:open_for
,
->
(
user
)
{
opened
.
assigned_to
(
user
)
}
scope
:in_projects
,
->
(
project_ids
)
{
where
(
project_id:
project_ids
)
}
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
state_machine
:state
,
initial: :opened
do
event
:close
do
event
:close
do
transition
[
:reopened
,
:opened
]
=>
:closed
transition
[
:reopened
,
:opened
]
=>
:closed
...
@@ -82,6 +96,15 @@ class Issue < ActiveRecord::Base
...
@@ -82,6 +96,15 @@ class Issue < ActiveRecord::Base
@link_reference_pattern
||=
super
(
"issues"
,
/(?<issue>\d+)/
)
@link_reference_pattern
||=
super
(
"issues"
,
/(?<issue>\d+)/
)
end
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
)
def
to_reference
(
from_project
=
nil
)
reference
=
"
#{
self
.
class
.
reference_prefix
}#{
iid
}
"
reference
=
"
#{
self
.
class
.
reference_prefix
}#{
iid
}
"
...
@@ -169,4 +192,8 @@ class Issue < ActiveRecord::Base
...
@@ -169,4 +192,8 @@ class Issue < ActiveRecord::Base
self
.
related_branches
(
current_user
).
empty?
&&
self
.
related_branches
(
current_user
).
empty?
&&
self
.
closed_by_merge_requests
(
current_user
).
empty?
self
.
closed_by_merge_requests
(
current_user
).
empty?
end
end
def
overdue?
due_date
.
try
(
:past?
)
||
false
end
end
end
app/models/label.rb
View file @
7e0ef892
...
@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base
...
@@ -113,6 +113,10 @@ class Label < ActiveRecord::Base
template
template
end
end
def
text_color
LabelsHelper
::
text_color_for_bg
(
self
.
color
)
end
private
private
def
label_format_reference
(
format
=
:id
)
def
label_format_reference
(
format
=
:id
)
...
...
app/views/projects/_activity.html.haml
View file @
7e0ef892
...
@@ -9,4 +9,7 @@
...
@@ -9,4 +9,7 @@
=
spinner
=
spinner
:javascript
:javascript
new
Activities
();
var
activity
=
new
Activities
();
$
(
document
).
on
(
'
page:restore
'
,
function
(
event
)
{
activity
.
reloadActivities
()
})
app/views/projects/issues/_issue.html.haml
View file @
7e0ef892
...
@@ -48,6 +48,11 @@
...
@@ -48,6 +48,11 @@
=
link_to
namespace_project_issues_path
(
issue
.
project
.
namespace
,
issue
.
project
,
milestone_title:
issue
.
milestone
.
title
)
do
=
link_to
namespace_project_issues_path
(
issue
.
project
.
namespace
,
issue
.
project
,
milestone_title:
issue
.
milestone
.
title
)
do
=
icon
(
'clock-o'
)
=
icon
(
'clock-o'
)
=
issue
.
milestone
.
title
=
issue
.
milestone
.
title
-
if
issue
.
due_date
%span
{
class:
"#{'cred' if issue.overdue?}"
}
=
icon
(
'calendar'
)
=
issue
.
due_date
.
to_s
(
:medium
)
-
if
issue
.
labels
.
any?
-
if
issue
.
labels
.
any?
-
issue
.
labels
.
each
do
|
label
|
-
issue
.
labels
.
each
do
|
label
|
...
...
app/views/projects/merge_requests/show/_mr_title.html.haml
View file @
7e0ef892
...
@@ -24,9 +24,9 @@
...
@@ -24,9 +24,9 @@
%li
{
class:
issue_button_visibility
(
@merge_request
,
false
)
}
%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'
=
link_to
'Reopen'
,
merge_request_path
(
@merge_request
,
merge_request:
{
state_event: :reopen
}),
method: :put
,
class:
'reopen-mr-link'
,
title:
'Reopen merge request'
%li
%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
'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
'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'
)
=
icon
(
'pencil-square-o'
)
Edit
Edit
app/views/shared/_label_row.html.haml
View file @
7e0ef892
...
@@ -2,4 +2,4 @@
...
@@ -2,4 +2,4 @@
%span
.label-name
%span
.label-name
=
link_to_label
(
label
,
tooltip:
false
)
=
link_to_label
(
label
,
tooltip:
false
)
%span
.prepend-left-10
%span
.prepend-left-10
=
markdown
(
label
.
description
,
pipeline: :single_line
)
=
markdown
(
label
.
description
,
pipeline: :single_line
)
\ No newline at end of file
app/views/shared/_labels_row.html.haml
0 → 100644
View file @
7e0ef892
-
labels
.
each
do
|
label
|
%span
.label-row
=
link_to_label
(
label
,
tooltip:
false
)
app/views/shared/_sort_dropdown.html.haml
View file @
7e0ef892
...
@@ -20,6 +20,11 @@
...
@@ -20,6 +20,11 @@
=
sort_title_milestone_soon
=
sort_title_milestone_soon
=
link_to
page_filter_path
(
sort:
sort_value_milestone_later
)
do
=
link_to
page_filter_path
(
sort:
sort_value_milestone_later
)
do
=
sort_title_milestone_later
=
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
=
link_to
page_filter_path
(
sort:
sort_value_upvotes
)
do
=
sort_title_upvotes
=
sort_title_upvotes
=
link_to
page_filter_path
(
sort:
sort_value_downvotes
)
do
=
link_to
page_filter_path
(
sort:
sort_value_downvotes
)
do
...
...
app/views/shared/issuable/_filter.html.haml
View file @
7e0ef892
...
@@ -23,6 +23,7 @@
...
@@ -23,6 +23,7 @@
.filter-item.inline.labels-filter
.filter-item.inline.labels-filter
=
render
"shared/issuable/label_dropdown"
=
render
"shared/issuable/label_dropdown"
.pull-right
.pull-right
=
render
'shared/sort_dropdown'
=
render
'shared/sort_dropdown'
...
@@ -46,9 +47,10 @@
...
@@ -46,9 +47,10 @@
.filter-item.inline
.filter-item.inline
=
button_tag
"Update issues"
,
class:
"btn update_selected_issues btn-save"
=
button_tag
"Update issues"
,
class:
"btn update_selected_issues btn-save"
-
if
@label
-
if
!
@labels
.
nil?
.gray-content-block.second-block
.gray-content-block.second-block.filtered-labels
{
class:
(
"hidden"
if
!
@labels
.
any?
)
}
=
render
"shared/label_row"
,
label:
@label
-
if
@labels
.
any?
=
render
"shared/labels_row"
,
labels:
@labels
:javascript
:javascript
new
UsersSelect
();
new
UsersSelect
();
...
...
app/views/shared/issuable/_label_dropdown.html.haml
View file @
7e0ef892
-
if
params
[
:label_name
].
present?
-
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
.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
%span
.dropdown-toggle-text
=
h
(
params
[
:label_name
].
presence
||
"Label"
)
=
h
(
multi_label_name
(
params
[
:label_name
],
"Label"
)
)
=
icon
(
'chevron-down'
)
=
icon
(
'chevron-down'
)
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
=
render
partial:
"shared/issuable/label_page_default"
,
locals:
{
title:
"Filter by label"
}
=
render
partial:
"shared/issuable/label_page_default"
,
locals:
{
title:
"Filter by label"
}
...
...
app/views/shared/issuable/_nav.html.haml
View file @
7e0ef892
...
@@ -4,22 +4,22 @@
...
@@ -4,22 +4,22 @@
-
else
-
else
-
page_context_word
=
'issues'
-
page_context_word
=
'issues'
%li
{
class:
(
"active"
if
params
[
:state
]
==
'opened'
)}
%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
)
}
#{
state_filters_text_for
(
:opened
,
@project
)
}
-
if
defined?
(
type
)
&&
type
==
:merge_requests
-
if
defined?
(
type
)
&&
type
==
:merge_requests
%li
{
class:
(
"active"
if
params
[
:state
]
==
'merged'
)}
%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
)
}
#{
state_filters_text_for
(
:merged
,
@project
)
}
%li
{
class:
(
"active"
if
params
[
:state
]
==
'closed'
)}
%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
)
}
#{
state_filters_text_for
(
:closed
,
@project
)
}
-
else
-
else
%li
{
class:
(
"active"
if
params
[
:state
]
==
'closed'
)}
%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
)
}
#{
state_filters_text_for
(
:closed
,
@project
)
}
%li
{
class:
(
"active"
if
params
[
:state
]
==
'all'
)}
%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
)
}
#{
state_filters_text_for
(
:all
,
@project
)
}
app/views/shared/issuable/_sidebar.html.haml
View file @
7e0ef892
...
@@ -10,14 +10,14 @@
...
@@ -10,14 +10,14 @@
=
sidebar_gutter_toggle_icon
=
sidebar_gutter_toggle_icon
.issuable-nav.hide-collapsed.pull-right.btn-group
{
role:
'group'
,
"aria-label"
=>
'...'
}
.issuable-nav.hide-collapsed.pull-right.btn-group
{
role:
'group'
,
"aria-label"
=>
'...'
}
-
if
prev_issuable
=
prev_issuable_for
(
issuable
)
-
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
-
else
%a
.btn.btn-default.disabled
{
href:
'#'
}
%a
.btn.btn-default.
issuable-pager.
disabled
{
href:
'#'
}
Prev
Prev
-
if
next_issuable
=
next_issuable_for
(
issuable
)
-
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
-
else
%a
.btn.btn-default.disabled
{
href:
'#'
}
%a
.btn.btn-default.
issuable-pager.
disabled
{
href:
'#'
}
Next
Next
=
form_for
[
@project
.
namespace
.
becomes
(
Namespace
),
@project
,
issuable
],
remote:
true
,
html:
{
class:
'issuable-context-form inline-update js-issuable-update'
}
do
|
f
|
=
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 @@
...
@@ -58,7 +58,7 @@
-
if
issuable
.
milestone
-
if
issuable
.
milestone
=
issuable
.
milestone
.
title
=
issuable
.
milestone
.
title
-
else
-
else
No
No
ne
.title.hide-collapsed
.title.hide-collapsed
Milestone
Milestone
=
icon
(
'spinner spin'
,
class:
'block-loading'
)
=
icon
(
'spinner spin'
,
class:
'block-loading'
)
...
@@ -75,6 +75,34 @@
...
@@ -75,6 +75,34 @@
=
f
.
hidden_field
'milestone_id'
,
value:
issuable
.
milestone_id
,
id:
nil
=
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
}})
=
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?
-
if
issuable
.
project
.
labels
.
any?
.block.labels
.block.labels
.sidebar-collapsed-icon
.sidebar-collapsed-icon
...
@@ -139,3 +167,4 @@
...
@@ -139,3 +167,4 @@
new
IssuableContext
(
'
#{
escape_javascript
(
current_user
.
to_json
(
only:
[
:username
,
:id
,
:name
]))
}
'
);
new
IssuableContext
(
'
#{
escape_javascript
(
current_user
.
to_json
(
only:
[
:username
,
:id
,
:name
]))
}
'
);
new
Subscription
(
'
.subscription
'
)
new
Subscription
(
'
.subscription
'
)
new
Sidebar
();
new
Sidebar
();
new
DueDateSelect
();
db/migrate/20160310124959_add_due_date_to_issues.rb
0 → 100644
View file @
7e0ef892
class
AddDueDateToIssues
<
ActiveRecord
::
Migration
def
change
add_column
:issues
,
:due_date
,
:date
add_index
:issues
,
:due_date
end
end
db/schema.rb
View file @
7e0ef892
...
@@ -422,6 +422,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
...
@@ -422,6 +422,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
t
.
integer
"moved_to_id"
t
.
integer
"moved_to_id"
t
.
boolean
"confidential"
,
default:
false
t
.
boolean
"confidential"
,
default:
false
t
.
datetime
"deleted_at"
t
.
datetime
"deleted_at"
t
.
date
"due_date"
end
end
add_index
"issues"
,
[
"assignee_id"
],
name:
"index_issues_on_assignee_id"
,
using: :btree
add_index
"issues"
,
[
"assignee_id"
],
name:
"index_issues_on_assignee_id"
,
using: :btree
...
@@ -431,6 +432,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
...
@@ -431,6 +432,7 @@ ActiveRecord::Schema.define(version: 20160419120017) do
add_index
"issues"
,
[
"created_at"
],
name:
"index_issues_on_created_at"
,
using: :btree
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"
,
[
"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"
,
[
"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"
,
[
"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"
,
"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
add_index
"issues"
,
[
"project_id"
],
name:
"index_issues_on_project_id"
,
using: :btree
...
...
features/project/issues/filter_labels.feature
View file @
7e0ef892
...
@@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels
...
@@ -12,6 +12,7 @@ Feature: Project Issues Filter Labels
@javascript
@javascript
Scenario
:
I
filter by one label
Scenario
:
I
filter by one label
Given
I click link
"bug"
Given
I click link
"bug"
And
I click
"dropdown close button"
Then
I should see
"Bugfix1"
in issues list
Then
I should see
"Bugfix1"
in issues list
And
I should see
"Bugfix2"
in issues list
And
I should see
"Bugfix2"
in issues list
And
I should not see
"Feature1"
in issues list
And
I should not see
"Feature1"
in issues list
...
...
features/steps/project/issues/filter_labels.rb
View file @
7e0ef892
...
@@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
...
@@ -32,6 +32,10 @@ class Spinach::Features::ProjectIssuesFilterLabels < Spinach::FeatureSteps
page
.
find
(
'.js-label-select'
).
click
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
bug
\"
) a').click()"
)
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
sleep
2
end
end
...
...
lib/api/internal.rb
View file @
7e0ef892
...
@@ -23,7 +23,7 @@ module API
...
@@ -23,7 +23,7 @@ module API
end
end
post
"/allowed"
do
post
"/allowed"
do
Gitlab
::
Metrics
.
tag_transaction
(
'action'
,
'Grape#/internal/allowed'
)
Gitlab
::
Metrics
.
action
=
'Grape#/internal/allowed'
status
200
status
200
...
...
lib/gitlab/gon_helper.rb
View file @
7e0ef892
...
@@ -7,6 +7,7 @@ module Gitlab
...
@@ -7,6 +7,7 @@ module Gitlab
gon
.
max_file_size
=
current_application_settings
.
max_attachment_size
gon
.
max_file_size
=
current_application_settings
.
max_attachment_size
gon
.
relative_url_root
=
Gitlab
.
config
.
gitlab
.
relative_url_root
gon
.
relative_url_root
=
Gitlab
.
config
.
gitlab
.
relative_url_root
gon
.
user_color_scheme
=
Gitlab
::
ColorSchemes
.
for_user
(
current_user
).
css_class
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
if
current_user
gon
.
current_user_id
=
current_user
.
id
gon
.
current_user_id
=
current_user
.
id
...
...
lib/gitlab/metrics.rb
View file @
7e0ef892
...
@@ -115,6 +115,15 @@ module Gitlab
...
@@ -115,6 +115,15 @@ module Gitlab
trans
.
add_tag
(
name
,
value
)
if
trans
trans
.
add_tag
(
name
,
value
)
if
trans
end
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
# When enabled this should be set before being used as the usual pattern
# "@foo ||= bar" is _not_ thread-safe.
# "@foo ||= bar" is _not_ thread-safe.
if
enabled?
if
enabled?
...
...
spec/features/issues/filter_by_labels_spec.rb
0 → 100644
View file @
7e0ef892
require
'rails_helper'
feature
'Issue filtering by Labels'
,
feature:
true
do
let
(
:project
)
{
create
(
:project
,
:public
)
}
let!
(
:user
)
{
create
(
:user
)}
let!
(
:label
)
{
create
(
:label
,
project:
project
)
}
before
do
[
'bug'
,
'feature'
,
'enhancement'
].
each
do
|
title
|
create
(
:label
,
project:
project
,
title:
title
)
end
issue1
=
create
(
:issue
,
title:
"Bugfix1"
,
project:
project
)
issue1
.
labels
<<
project
.
labels
.
find_by
(
title:
'bug'
)
issue2
=
create
(
:issue
,
title:
"Bugfix2"
,
project:
project
)
issue2
.
labels
<<
project
.
labels
.
find_by
(
title:
'bug'
)
issue2
.
labels
<<
project
.
labels
.
find_by
(
title:
'enhancement'
)
issue3
=
create
(
:issue
,
title:
"Feature1"
,
project:
project
)
issue3
.
labels
<<
project
.
labels
.
find_by
(
title:
'feature'
)
project
.
team
<<
[
user
,
:master
]
login_as
(
user
)
visit
namespace_project_issues_path
(
project
.
namespace
,
project
)
end
context
'filter by label bug'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
bug
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
it
'should show issue "Bugfix1" and "Bugfix2" in issues list'
do
expect
(
page
).
to
have_content
"Bugfix1"
expect
(
page
).
to
have_content
"Bugfix2"
end
it
'should not show "Feature1" in issues list'
do
expect
(
page
).
not_to
have_content
"Feature1"
end
it
'should show label "bug" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"bug"
end
it
'should not show label "feature" and "enhancement" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"feature"
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"enhancement"
end
end
context
'filter by label feature'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
feature
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
it
'should show issue "Feature1" in issues list'
do
expect
(
page
).
to
have_content
"Feature1"
end
it
'should not show "Bugfix1" and "Bugfix2" in issues list'
do
expect
(
page
).
not_to
have_content
"Bugfix2"
expect
(
page
).
not_to
have_content
"Bugfix1"
end
it
'should show label "feature" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"feature"
end
it
'should not show label "bug" and "enhancement" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"bug"
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"enhancement"
end
end
context
'filter by label enhancement'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
enhancement
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
it
'should show issue "Bugfix2" in issues list'
do
expect
(
page
).
to
have_content
"Bugfix2"
end
it
'should not show "Feature1" and "Bugfix1" in issues list'
do
expect
(
page
).
not_to
have_content
"Feature1"
expect
(
page
).
not_to
have_content
"Bugfix1"
end
it
'should show label "enhancement" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"enhancement"
end
it
'should not show label "feature" and "bug" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"bug"
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"feature"
end
end
context
'filter by label enhancement or feature'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
enhancement
\"
) a').click()"
)
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
feature
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
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"
end
it
'should show label "enhancement" and "feature" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"enhancement"
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"feature"
end
it
'should not show label "bug" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"bug"
end
end
context
'filter by label enhancement or bug in issues list'
,
js:
true
do
before
do
page
.
find
(
'.js-label-select'
).
click
sleep
0.5
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
enhancement
\"
) a').click()"
)
execute_script
(
"$('.dropdown-menu-labels li:contains(
\"
bug
\"
) a').click()"
)
page
.
first
(
'.labels-filter .dropdown-title .dropdown-menu-close-icon'
).
click
sleep
2
end
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
expect
(
page
).
not_to
have_content
"Feature1"
end
it
'should show label "bug" and "enhancement" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"bug"
expect
(
find
(
'.filtered-labels'
)).
to
have_content
"enhancement"
end
it
'should not show label "feature" in filtered-labels'
do
expect
(
find
(
'.filtered-labels'
)).
not_to
have_content
"feature"
end
end
end
spec/features/issues/filter_issues_spec.rb
View file @
7e0ef892
...
@@ -84,14 +84,20 @@ describe 'Filter issues', feature: true do
...
@@ -84,14 +84,20 @@ describe 'Filter issues', feature: true do
it
'should filter by any label'
do
it
'should filter by any label'
do
find
(
'.dropdown-menu-labels a'
,
text:
'Any Label'
).
click
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
page
.
within
'.labels-filter'
do
expect
(
page
).
to
have_content
'Any Label'
expect
(
page
).
to
have_content
'Any Label'
end
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
end
it
'should filter by no label'
do
it
'should filter by no label'
do
find
(
'.dropdown-menu-labels a'
,
text:
'No Label'
).
click
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
page
.
within
'.labels-filter'
do
expect
(
page
).
to
have_content
'No Label'
expect
(
page
).
to
have_content
'No Label'
end
end
...
@@ -121,6 +127,7 @@ describe 'Filter issues', feature: true do
...
@@ -121,6 +127,7 @@ describe 'Filter issues', feature: true do
find
(
'.js-label-select'
).
click
find
(
'.js-label-select'
).
click
find
(
'.dropdown-menu-labels .dropdown-content a'
,
text:
label
.
title
).
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
sleep
2
end
end
...
...
spec/features/issues_spec.rb
View file @
7e0ef892
...
@@ -112,7 +112,7 @@ describe 'Issues', feature: true do
...
@@ -112,7 +112,7 @@ describe 'Issues', feature: true do
end
end
describe
'filter issue'
do
describe
'filter issue'
do
titles
=
[
'foo'
,
'bar'
,
'baz'
]
titles
=
%w[foo bar baz
]
titles
.
each_with_index
do
|
title
,
index
|
titles
.
each_with_index
do
|
title
,
index
|
let!
(
title
.
to_sym
)
do
let!
(
title
.
to_sym
)
do
create
(
:issue
,
title:
title
,
create
(
:issue
,
title:
title
,
...
@@ -153,8 +153,94 @@ describe 'Issues', feature: true do
...
@@ -153,8 +153,94 @@ describe 'Issues', feature: true do
expect
(
first_issue
).
to
include
(
'baz'
)
expect
(
first_issue
).
to
include
(
'baz'
)
end
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
describe
'sorting by milestone'
do
before
:each
do
before
do
foo
.
milestone
=
newer_due_milestone
foo
.
milestone
=
newer_due_milestone
foo
.
save
foo
.
save
bar
.
milestone
=
later_due_milestone
bar
.
milestone
=
later_due_milestone
...
@@ -177,7 +263,7 @@ describe 'Issues', feature: true do
...
@@ -177,7 +263,7 @@ describe 'Issues', feature: true do
describe
'combine filter and sort'
do
describe
'combine filter and sort'
do
let
(
:user2
)
{
create
(
:user
)
}
let
(
:user2
)
{
create
(
:user
)
}
before
:each
do
before
do
foo
.
assignee
=
user2
foo
.
assignee
=
user2
foo
.
save
foo
.
save
bar
.
assignee
=
user2
bar
.
assignee
=
user2
...
@@ -224,7 +310,7 @@ describe 'Issues', feature: true do
...
@@ -224,7 +310,7 @@ describe 'Issues', feature: true do
let
(
:guest
)
{
create
(
:user
)
}
let
(
:guest
)
{
create
(
:user
)
}
before
:each
do
before
do
project
.
team
<<
[[
guest
],
:guest
]
project
.
team
<<
[[
guest
],
:guest
]
end
end
...
@@ -267,7 +353,7 @@ describe 'Issues', feature: true do
...
@@ -267,7 +353,7 @@ describe 'Issues', feature: true do
context
'by unauthorized user'
do
context
'by unauthorized user'
do
let
(
:guest
)
{
create
(
:user
)
}
let
(
:guest
)
{
create
(
:user
)
}
before
:each
do
before
do
project
.
team
<<
[
guest
,
:guest
]
project
.
team
<<
[
guest
,
:guest
]
issue
.
milestone
=
milestone
issue
.
milestone
=
milestone
issue
.
save
issue
.
save
...
@@ -285,7 +371,7 @@ describe 'Issues', feature: true do
...
@@ -285,7 +371,7 @@ describe 'Issues', feature: true do
describe
'removing assignee'
do
describe
'removing assignee'
do
let
(
:user2
)
{
create
(
:user
)
}
let
(
:user2
)
{
create
(
:user
)
}
before
:each
do
before
do
issue
.
assignee
=
user2
issue
.
assignee
=
user2
issue
.
save
issue
.
save
end
end
...
...
spec/features/merge_requests/filter_by_milestone_spec.rb
View file @
7e0ef892
...
@@ -2,8 +2,14 @@ require 'rails_helper'
...
@@ -2,8 +2,14 @@ require 'rails_helper'
feature
'Merge Request filtering by Milestone'
,
feature:
true
do
feature
'Merge Request filtering by Milestone'
,
feature:
true
do
let
(
:project
)
{
create
(
:project
,
:public
)
}
let
(
:project
)
{
create
(
:project
,
:public
)
}
let!
(
:user
)
{
create
(
:user
)}
let
(
:milestone
)
{
create
(
:milestone
,
project:
project
)
}
let
(
:milestone
)
{
create
(
:milestone
,
project:
project
)
}
before
do
project
.
team
<<
[
user
,
:master
]
login_as
(
user
)
end
scenario
'filters by no Milestone'
,
js:
true
do
scenario
'filters by no Milestone'
,
js:
true
do
create
(
:merge_request
,
:with_diffs
,
source_project:
project
)
create
(
:merge_request
,
:with_diffs
,
source_project:
project
)
create
(
:merge_request
,
:simple
,
source_project:
project
,
milestone:
milestone
)
create
(
:merge_request
,
:simple
,
source_project:
project
,
milestone:
milestone
)
...
...
spec/finders/issues_finder_spec.rb
View file @
7e0ef892
...
@@ -62,6 +62,22 @@ describe IssuesFinder do
...
@@ -62,6 +62,22 @@ describe IssuesFinder do
expect
(
issues
).
to
eq
([
issue2
])
expect
(
issues
).
to
eq
([
issue2
])
end
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
it
'should filter by no label name'
do
params
=
{
scope:
"all"
,
label_name:
Label
::
None
.
title
,
state:
'opened'
}
params
=
{
scope:
"all"
,
label_name:
Label
::
None
.
title
,
state:
'opened'
}
issues
=
IssuesFinder
.
new
(
user
,
params
).
execute
issues
=
IssuesFinder
.
new
(
user
,
params
).
execute
...
...
spec/lib/gitlab/metrics_spec.rb
View file @
7e0ef892
...
@@ -123,4 +123,28 @@ describe Gitlab::Metrics do
...
@@ -123,4 +123,28 @@ describe Gitlab::Metrics do
end
end
end
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
end
vendor/assets/javascripts/raven.js
0 → 100644
View file @
7e0ef892
/*! 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
(
'
<
'
,
'
(?:<|<)
'
).
replace
(
'
>
'
,
'
(?:>|>)
'
).
replace
(
'
&
'
,
'
(?:&|&)
'
).
replace
(
'
"
'
,
'
(?:"|")
'
).
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
\x
A0-
\u
FFFF
][
_$a-zA-Z0-9
\x
A0-
\u
FFFF
]
*
)?\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
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment