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
1
Merge Requests
1
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
nexedi
gitlab-ce
Commits
e95a9759
Commit
e95a9759
authored
Oct 25, 2019
by
GitLab Bot
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Add latest changes from gitlab-org/gitlab@master
parent
6ac3c679
Changes
17
Show whitespace changes
Inline
Side-by-side
Showing
17 changed files
with
173 additions
and
29 deletions
+173
-29
app/assets/javascripts/error_tracking/components/error_tracking_list.vue
...scripts/error_tracking/components/error_tracking_list.vue
+36
-5
app/assets/javascripts/error_tracking/store/getters.js
app/assets/javascripts/error_tracking/store/getters.js
+4
-0
app/assets/javascripts/error_tracking/store/index.js
app/assets/javascripts/error_tracking/store/index.js
+2
-0
app/controllers/projects/pipelines_controller.rb
app/controllers/projects/pipelines_controller.rb
+14
-7
app/views/projects/blame/show.html.haml
app/views/projects/blame/show.html.haml
+1
-1
changelogs/unreleased/drop-id-of-ci-trace-sectinos.yml
changelogs/unreleased/drop-id-of-ci-trace-sectinos.yml
+5
-0
changelogs/unreleased/jc-dont-render-commit-links.yml
changelogs/unreleased/jc-dont-render-commit-links.yml
+5
-0
changelogs/unreleased/lm-search-list-of-sentry-errors.yml
changelogs/unreleased/lm-search-list-of-sentry-errors.yml
+5
-0
changelogs/unreleased/sh-gitaly-duration-measurement.yml
changelogs/unreleased/sh-gitaly-duration-measurement.yml
+5
-0
db/post_migrate/20191017180026_drop_ci_build_trace_sections_id.rb
...migrate/20191017180026_drop_ci_build_trace_sections_id.rb
+19
-0
db/schema.rb
db/schema.rb
+2
-2
doc/user/project/operations/error_tracking.md
doc/user/project/operations/error_tracking.md
+1
-0
doc/user/project/operations/img/error_tracking_list.png
doc/user/project/operations/img/error_tracking_list.png
+0
-0
lib/gitlab/gitaly_client.rb
lib/gitlab/gitaly_client.rb
+24
-3
lib/gitlab/gitaly_client/commit_service.rb
lib/gitlab/gitaly_client/commit_service.rb
+15
-11
spec/frontend/error_tracking/store/getters_spec.js
spec/frontend/error_tracking/store/getters_spec.js
+33
-0
spec/lib/gitlab/gitaly_client_spec.rb
spec/lib/gitlab/gitaly_client_spec.rb
+2
-0
No files found.
app/assets/javascripts/error_tracking/components/error_tracking_list.vue
View file @
e95a9759
<
script
>
<
script
>
import
{
mapActions
,
mapState
}
from
'
vuex
'
;
import
{
mapActions
,
mapState
,
mapGetters
}
from
'
vuex
'
;
import
{
GlEmptyState
,
GlButton
,
GlLink
,
GlLoadingIcon
,
GlTable
}
from
'
@gitlab/ui
'
;
import
{
GlEmptyState
,
GlButton
,
GlLink
,
GlLoadingIcon
,
GlTable
,
GlSearchBoxByType
,
}
from
'
@gitlab/ui
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
Icon
from
'
~/vue_shared/components/icon.vue
'
;
import
TimeAgo
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
TimeAgo
from
'
~/vue_shared/components/time_ago_tooltip.vue
'
;
import
{
__
}
from
'
~/locale
'
;
import
{
__
}
from
'
~/locale
'
;
...
@@ -20,6 +27,7 @@ export default {
...
@@ -20,6 +27,7 @@ export default {
GlLink
,
GlLink
,
GlLoadingIcon
,
GlLoadingIcon
,
GlTable
,
GlTable
,
GlSearchBoxByType
,
Icon
,
Icon
,
TimeAgo
,
TimeAgo
,
},
},
...
@@ -48,8 +56,17 @@ export default {
...
@@ -48,8 +56,17 @@ export default {
required
:
true
,
required
:
true
,
},
},
},
},
data
()
{
return
{
errorSearchQuery
:
''
,
};
},
computed
:
{
computed
:
{
...
mapState
([
'
errors
'
,
'
externalUrl
'
,
'
loading
'
]),
...
mapState
([
'
errors
'
,
'
externalUrl
'
,
'
loading
'
]),
...
mapGetters
([
'
filterErrorsByTitle
'
]),
filteredErrors
()
{
return
this
.
errorSearchQuery
?
this
.
filterErrorsByTitle
(
this
.
errorSearchQuery
)
:
this
.
errors
;
},
},
},
created
()
{
created
()
{
if
(
this
.
errorTrackingEnabled
)
{
if
(
this
.
errorTrackingEnabled
)
{
...
@@ -71,10 +88,17 @@ export default {
...
@@ -71,10 +88,17 @@ export default {
<gl-loading-icon
:size=
"3"
/>
<gl-loading-icon
:size=
"3"
/>
</div>
</div>
<div
v-else
>
<div
v-else
>
<div
class=
"d-flex justify-content-end"
>
<div
class=
"d-flex flex-row justify-content-around bg-secondary border"
>
<gl-search-box-by-type
v-model=
"errorSearchQuery"
class=
"col-lg-10 m-3 p-0"
:placeholder=
"__('Search or filter results...')"
type=
"search"
autofocus
/>
<gl-button
<gl-button
v-track-event=
"trackViewInSentryOptions(externalUrl)"
v-track-event=
"trackViewInSentryOptions(externalUrl)"
class=
"m
y-3 ml-auto
"
class=
"m
-3
"
variant=
"primary"
variant=
"primary"
:href=
"externalUrl"
:href=
"externalUrl"
target=
"_blank"
target=
"_blank"
...
@@ -84,7 +108,14 @@ export default {
...
@@ -84,7 +108,14 @@ export default {
</gl-button>
</gl-button>
</div>
</div>
<gl-table
:items=
"errors"
:fields=
"$options.fields"
:show-empty=
"true"
fixed
stacked=
"sm"
>
<gl-table
class=
"mt-3"
:items=
"filteredErrors"
:fields=
"$options.fields"
:show-empty=
"true"
fixed
stacked=
"sm"
>
<template
slot=
"HEAD_events"
slot-scope=
"data"
>
<template
slot=
"HEAD_events"
slot-scope=
"data"
>
<div
class=
"text-md-right"
>
{{
data
.
label
}}
</div>
<div
class=
"text-md-right"
>
{{
data
.
label
}}
</div>
</
template
>
</
template
>
...
...
app/assets/javascripts/error_tracking/store/getters.js
0 → 100644
View file @
e95a9759
export
const
filterErrorsByTitle
=
state
=>
errorQuery
=>
state
.
errors
.
filter
(
error
=>
error
.
title
.
match
(
new
RegExp
(
`
${
errorQuery
}
`
,
'
i
'
)));
export
default
()
=>
{};
app/assets/javascripts/error_tracking/store/index.js
View file @
e95a9759
import
Vue
from
'
vue
'
;
import
Vue
from
'
vue
'
;
import
Vuex
from
'
vuex
'
;
import
Vuex
from
'
vuex
'
;
import
*
as
actions
from
'
./actions
'
;
import
*
as
actions
from
'
./actions
'
;
import
*
as
getters
from
'
./getters
'
;
import
mutations
from
'
./mutations
'
;
import
mutations
from
'
./mutations
'
;
Vue
.
use
(
Vuex
);
Vue
.
use
(
Vuex
);
...
@@ -14,6 +15,7 @@ export const createStore = () =>
...
@@ -14,6 +15,7 @@ export const createStore = () =>
},
},
actions
,
actions
,
mutations
,
mutations
,
getters
,
});
});
export
default
createStore
();
export
default
createStore
();
app/controllers/projects/pipelines_controller.rb
View file @
e95a9759
...
@@ -156,15 +156,22 @@ class Projects::PipelinesController < Projects::ApplicationController
...
@@ -156,15 +156,22 @@ class Projects::PipelinesController < Projects::ApplicationController
def
test_report
def
test_report
return
unless
Feature
.
enabled?
(
:junit_pipeline_view
,
project
)
return
unless
Feature
.
enabled?
(
:junit_pipeline_view
,
project
)
if
pipeline_test_report
==
:error
respond_to
do
|
format
|
render
json:
{
status: :error_parsing_report
}
format
.
html
do
return
render
'show'
end
end
format
.
json
do
if
pipeline_test_report
==
:error
render
json:
{
status: :error_parsing_report
}
else
render
json:
TestReportSerializer
render
json:
TestReportSerializer
.
new
(
current_user:
@current_user
)
.
new
(
current_user:
@current_user
)
.
represent
(
pipeline_test_report
)
.
represent
(
pipeline_test_report
)
end
end
end
end
end
private
private
...
...
app/views/projects/blame/show.html.haml
View file @
e95a9759
...
@@ -19,7 +19,7 @@
...
@@ -19,7 +19,7 @@
=
author_avatar
(
commit
,
size:
36
,
has_tooltip:
false
)
=
author_avatar
(
commit
,
size:
36
,
has_tooltip:
false
)
.commit-row-title
.commit-row-title
%span
.item-title.str-truncated-100
%span
.item-title.str-truncated-100
=
link_to
_markdown
commit
.
title
,
project_commit_path
(
@project
,
commit
.
id
),
class:
"cdark"
,
title:
commit
.
title
=
link_to
commit
.
title
,
project_commit_path
(
@project
,
commit
.
id
),
class:
"cdark"
,
title:
commit
.
title
.float-right
.float-right
=
link_to
commit
.
short_id
,
project_commit_path
(
@project
,
commit
),
class:
"commit-sha"
=
link_to
commit
.
short_id
,
project_commit_path
(
@project
,
commit
),
class:
"commit-sha"
...
...
changelogs/unreleased/drop-id-of-ci-trace-sectinos.yml
0 → 100644
View file @
e95a9759
---
title
:
Drop `id` column from `ci_build_trace_sections` table
merge_request
:
18741
author
:
type
:
changed
changelogs/unreleased/jc-dont-render-commit-links.yml
0 → 100644
View file @
e95a9759
---
title
:
Do not render links in commit message on blame page
merge_request
:
19128
author
:
type
:
performance
changelogs/unreleased/lm-search-list-of-sentry-errors.yml
0 → 100644
View file @
e95a9759
---
title
:
Search list of Sentry errors by title in Gitlab
merge_request
:
18772
author
:
type
:
added
changelogs/unreleased/sh-gitaly-duration-measurement.yml
0 → 100644
View file @
e95a9759
---
title
:
Fix Gitaly call duration measurements
merge_request
:
18785
author
:
type
:
fixed
db/post_migrate/20191017180026_drop_ci_build_trace_sections_id.rb
0 → 100644
View file @
e95a9759
# frozen_string_literal: true
class
DropCiBuildTraceSectionsId
<
ActiveRecord
::
Migration
[
5.2
]
DOWNTIME
=
false
def
up
##
# This column has already been ignored since 12.4
# See https://gitlab.com/gitlab-org/gitlab/issues/32569
remove_column
:ci_build_trace_sections
,
:id
end
def
down
##
# We don't backfill serial ids as it's not used in application code
# and quite expensive process.
add_column
:ci_build_trace_sections
,
:id
,
:bigint
end
end
db/schema.rb
View file @
e95a9759
...
@@ -10,7 +10,7 @@
...
@@ -10,7 +10,7 @@
#
#
# It's strongly recommended that you check this file into your version control system.
# It's strongly recommended that you check this file into your version control system.
ActiveRecord
::
Schema
.
define
(
version:
2019_10_17_
094449
)
do
ActiveRecord
::
Schema
.
define
(
version:
2019_10_17_
180026
)
do
# These are extensions that must be enabled in order to support this database
# These are extensions that must be enabled in order to support this database
enable_extension
"pg_trgm"
enable_extension
"pg_trgm"
...
@@ -599,7 +599,7 @@ ActiveRecord::Schema.define(version: 2019_10_17_094449) do
...
@@ -599,7 +599,7 @@ ActiveRecord::Schema.define(version: 2019_10_17_094449) do
t
.
index
[
"project_id"
,
"name"
],
name:
"index_ci_build_trace_section_names_on_project_id_and_name"
,
unique:
true
t
.
index
[
"project_id"
,
"name"
],
name:
"index_ci_build_trace_section_names_on_project_id_and_name"
,
unique:
true
end
end
create_table
"ci_build_trace_sections"
,
id:
:serial
,
force: :cascade
do
|
t
|
create_table
"ci_build_trace_sections"
,
id:
false
,
force: :cascade
do
|
t
|
t
.
integer
"project_id"
,
null:
false
t
.
integer
"project_id"
,
null:
false
t
.
datetime
"date_start"
,
null:
false
t
.
datetime
"date_start"
,
null:
false
t
.
datetime
"date_end"
,
null:
false
t
.
datetime
"date_end"
,
null:
false
...
...
doc/user/project/operations/error_tracking.md
View file @
e95a9759
...
@@ -41,5 +41,6 @@ NOTE: **Note:**
...
@@ -41,5 +41,6 @@ NOTE: **Note:**
You will need at least Reporter
[
permissions
](
../../permissions.md
)
to view the Error Tracking list.
You will need at least Reporter
[
permissions
](
../../permissions.md
)
to view the Error Tracking list.
The Error Tracking list may be found at
**Operations > Error Tracking**
in your project's sidebar.
The Error Tracking list may be found at
**Operations > Error Tracking**
in your project's sidebar.
Errors can be filtered by title.
![
Error Tracking list
](
img/error_tracking_list.png
)
![
Error Tracking list
](
img/error_tracking_list.png
)
doc/user/project/operations/img/error_tracking_list.png
View replaced file @
6ac3c679
View file @
e95a9759
59.3 KB
|
W:
|
H:
743 KB
|
W:
|
H:
2-up
Swipe
Onion skin
lib/gitlab/gitaly_client.rb
View file @
e95a9759
...
@@ -142,18 +142,39 @@ module Gitlab
...
@@ -142,18 +142,39 @@ module Gitlab
# kwargs.merge(deadline: Time.now + 10)
# kwargs.merge(deadline: Time.now + 10)
# end
# end
#
#
def
self
.
call
(
storage
,
service
,
rpc
,
request
,
remote_storage:
nil
,
timeout:
default_timeout
)
def
self
.
call
(
storage
,
service
,
rpc
,
request
,
remote_storage:
nil
,
timeout:
default_timeout
,
&
block
)
start
=
Gitlab
::
Metrics
::
System
.
monotonic_time
self
.
measure_timings
(
service
,
rpc
,
request
)
do
request_hash
=
request
.
is_a?
(
Google
::
Protobuf
::
MessageExts
)
?
request
.
to_h
:
{}
self
.
execute
(
storage
,
service
,
rpc
,
request
,
remote_storage:
remote_storage
,
timeout:
timeout
,
&
block
)
end
end
# This method is like GitalyClient.call but should be used with
# Gitaly streaming RPCs. It measures how long the the RPC took to
# produce the full response, not just the initial response.
def
self
.
streaming_call
(
storage
,
service
,
rpc
,
request
,
remote_storage:
nil
,
timeout:
default_timeout
)
self
.
measure_timings
(
service
,
rpc
,
request
)
do
response
=
self
.
execute
(
storage
,
service
,
rpc
,
request
,
remote_storage:
remote_storage
,
timeout:
timeout
)
yield
(
response
)
end
end
def
self
.
execute
(
storage
,
service
,
rpc
,
request
,
remote_storage
:,
timeout
:)
enforce_gitaly_request_limits
(
:call
)
enforce_gitaly_request_limits
(
:call
)
kwargs
=
request_kwargs
(
storage
,
timeout:
timeout
.
to_f
,
remote_storage:
remote_storage
)
kwargs
=
request_kwargs
(
storage
,
timeout:
timeout
.
to_f
,
remote_storage:
remote_storage
)
kwargs
=
yield
(
kwargs
)
if
block_given?
kwargs
=
yield
(
kwargs
)
if
block_given?
stub
(
service
,
storage
).
__send__
(
rpc
,
request
,
kwargs
)
# rubocop:disable GitlabSecurity/PublicSend
stub
(
service
,
storage
).
__send__
(
rpc
,
request
,
kwargs
)
# rubocop:disable GitlabSecurity/PublicSend
end
def
self
.
measure_timings
(
service
,
rpc
,
request
)
start
=
Gitlab
::
Metrics
::
System
.
monotonic_time
yield
ensure
ensure
duration
=
Gitlab
::
Metrics
::
System
.
monotonic_time
-
start
duration
=
Gitlab
::
Metrics
::
System
.
monotonic_time
-
start
request_hash
=
request
.
is_a?
(
Google
::
Protobuf
::
MessageExts
)
?
request
.
to_h
:
{}
# Keep track, separately, for the performance bar
# Keep track, separately, for the performance bar
self
.
query_time
+=
duration
self
.
query_time
+=
duration
...
...
lib/gitlab/gitaly_client/commit_service.rb
View file @
e95a9759
...
@@ -200,9 +200,10 @@ module Gitlab
...
@@ -200,9 +200,10 @@ module Gitlab
to:
to
to:
to
)
)
response
=
GitalyClient
.
call
(
@repository
.
storage
,
:commit_service
,
:commits_between
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
GitalyClient
.
streaming_call
(
@repository
.
storage
,
:commit_service
,
:commits_between
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
do
|
response
|
consume_commits_response
(
response
)
consume_commits_response
(
response
)
end
end
end
def
diff_stats
(
left_commit_sha
,
right_commit_sha
)
def
diff_stats
(
left_commit_sha
,
right_commit_sha
)
request
=
Gitaly
::
DiffStatsRequest
.
new
(
request
=
Gitaly
::
DiffStatsRequest
.
new
(
...
@@ -224,17 +225,19 @@ module Gitlab
...
@@ -224,17 +225,19 @@ module Gitlab
)
)
request
.
order
=
opts
[
:order
].
upcase
if
opts
[
:order
].
present?
request
.
order
=
opts
[
:order
].
upcase
if
opts
[
:order
].
present?
response
=
GitalyClient
.
call
(
@repository
.
storage
,
:commit_service
,
:find_all_commits
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
GitalyClient
.
streaming_call
(
@repository
.
storage
,
:commit_service
,
:find_all_commits
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
do
|
response
|
consume_commits_response
(
response
)
consume_commits_response
(
response
)
end
end
end
def
list_commits_by_oid
(
oids
)
def
list_commits_by_oid
(
oids
)
return
[]
if
oids
.
empty?
return
[]
if
oids
.
empty?
request
=
Gitaly
::
ListCommitsByOidRequest
.
new
(
repository:
@gitaly_repo
,
oid:
oids
)
request
=
Gitaly
::
ListCommitsByOidRequest
.
new
(
repository:
@gitaly_repo
,
oid:
oids
)
response
=
GitalyClient
.
call
(
@repository
.
storage
,
:commit_service
,
:list_commits_by_oid
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
GitalyClient
.
streaming_call
(
@repository
.
storage
,
:commit_service
,
:list_commits_by_oid
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
do
|
response
|
consume_commits_response
(
response
)
consume_commits_response
(
response
)
end
rescue
GRPC
::
NotFound
# If no repository is found, happens mainly during testing
rescue
GRPC
::
NotFound
# If no repository is found, happens mainly during testing
[]
[]
end
end
...
@@ -249,9 +252,10 @@ module Gitlab
...
@@ -249,9 +252,10 @@ module Gitlab
offset:
offset
.
to_i
offset:
offset
.
to_i
)
)
response
=
GitalyClient
.
call
(
@repository
.
storage
,
:commit_service
,
:commits_by_message
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
GitalyClient
.
streaming_call
(
@repository
.
storage
,
:commit_service
,
:commits_by_message
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
do
|
response
|
consume_commits_response
(
response
)
consume_commits_response
(
response
)
end
end
end
def
languages
(
ref
=
nil
)
def
languages
(
ref
=
nil
)
request
=
Gitaly
::
CommitLanguagesRequest
.
new
(
repository:
@gitaly_repo
,
revision:
ref
||
''
)
request
=
Gitaly
::
CommitLanguagesRequest
.
new
(
repository:
@gitaly_repo
,
revision:
ref
||
''
)
...
@@ -323,10 +327,10 @@ module Gitlab
...
@@ -323,10 +327,10 @@ module Gitlab
request
.
paths
=
encode_repeated
(
Array
(
options
[
:path
]))
if
options
[
:path
].
present?
request
.
paths
=
encode_repeated
(
Array
(
options
[
:path
]))
if
options
[
:path
].
present?
response
=
GitalyClient
.
call
(
@repository
.
storage
,
:commit_service
,
:find_commits
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
GitalyClient
.
streaming_call
(
@repository
.
storage
,
:commit_service
,
:find_commits
,
request
,
timeout:
GitalyClient
.
medium_timeout
)
do
|
response
|
consume_commits_response
(
response
)
consume_commits_response
(
response
)
end
end
end
def
filter_shas_with_signatures
(
shas
)
def
filter_shas_with_signatures
(
shas
)
request
=
Gitaly
::
FilterShasWithSignaturesRequest
.
new
(
repository:
@gitaly_repo
)
request
=
Gitaly
::
FilterShasWithSignaturesRequest
.
new
(
repository:
@gitaly_repo
)
...
...
spec/frontend/error_tracking/store/getters_spec.js
0 → 100644
View file @
e95a9759
import
*
as
getters
from
'
~/error_tracking/store/getters
'
;
describe
(
'
Error Tracking getters
'
,
()
=>
{
let
state
;
const
mockErrors
=
[
{
title
:
'
ActiveModel::MissingAttributeError: missing attribute: encrypted_password
'
},
{
title
:
'
Grape::Exceptions::MethodNotAllowed: Grape::Exceptions::MethodNotAllowed
'
},
{
title
:
'
NoMethodError: undefined method `sanitize_http_headers=
'
},
{
title
:
'
NoMethodError: undefined method `pry
'
},
];
beforeEach
(()
=>
{
state
=
{
errors
:
mockErrors
,
};
});
describe
(
'
search results
'
,
()
=>
{
it
(
'
should return errors filtered by words in title matching the query
'
,
()
=>
{
const
filteredErrors
=
getters
.
filterErrorsByTitle
(
state
)(
'
NoMethod
'
);
expect
(
filteredErrors
).
not
.
toContainEqual
(
mockErrors
[
0
]);
expect
(
filteredErrors
.
length
).
toBe
(
2
);
});
it
(
'
should not return results if there is no matching query
'
,
()
=>
{
const
filteredErrors
=
getters
.
filterErrorsByTitle
(
state
)(
'
GitLab
'
);
expect
(
filteredErrors
.
length
).
toBe
(
0
);
});
});
});
spec/lib/gitlab/gitaly_client_spec.rb
View file @
e95a9759
...
@@ -377,6 +377,8 @@ describe Gitlab::GitalyClient do
...
@@ -377,6 +377,8 @@ describe Gitlab::GitalyClient do
context
'when the request store is active'
,
:request_store
do
context
'when the request store is active'
,
:request_store
do
it
'records call details if a RPC is called'
do
it
'records call details if a RPC is called'
do
expect
(
described_class
).
to
receive
(
:measure_timings
).
and_call_original
gitaly_server
.
server_version
gitaly_server
.
server_version
expect
(
described_class
.
list_call_details
).
not_to
be_empty
expect
(
described_class
.
list_call_details
).
not_to
be_empty
...
...
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