Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
A
apollo
Project
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
openSource
apollo
Commits
954a562e
Commit
954a562e
authored
Apr 15, 2016
by
Jason Song
Browse files
Options
Browse Files
Download
Plain Diff
Merge pull request #98 from yiming187/release_fix
Ingore empty item when build release
parents
a5ff9a98
6f3ad2c9
Hide whitespace changes
Inline
Side-by-side
Showing
7 changed files
with
69 additions
and
51 deletions
+69
-51
pom.xml
apollo-adminservice/pom.xml
+5
-0
ReleaseService.java
...ain/java/com/ctrip/apollo/biz/service/ReleaseService.java
+4
-0
ConfigIntegrationTest.java
...a/com/ctrip/apollo/integration/ConfigIntegrationTest.java
+42
-33
AdminServiceAPI.java
...ain/java/com/ctrip/apollo/portal/api/AdminServiceAPI.java
+1
-1
ConfigController.java
.../com/ctrip/apollo/portal/controller/ConfigController.java
+1
-3
ConfigService.java
...n/java/com/ctrip/apollo/portal/service/ConfigService.java
+15
-13
PropertyResolver.java
...p/apollo/portal/service/txtresolver/PropertyResolver.java
+1
-1
No files found.
apollo-adminservice/pom.xml
View file @
954a562e
...
@@ -64,6 +64,11 @@
...
@@ -64,6 +64,11 @@
</exclusion>
</exclusion>
</exclusions>
</exclusions>
</dependency>
</dependency>
<dependency>
<groupId>
com.h2database
</groupId>
<artifactId>
h2
</artifactId>
<scope>
test
</scope>
</dependency>
</dependencies>
</dependencies>
<build>
<build>
<plugins>
<plugins>
...
...
apollo-biz/src/main/java/com/ctrip/apollo/biz/service/ReleaseService.java
View file @
954a562e
...
@@ -15,6 +15,7 @@ import com.ctrip.apollo.biz.repository.ItemRepository;
...
@@ -15,6 +15,7 @@ import com.ctrip.apollo.biz.repository.ItemRepository;
import
com.ctrip.apollo.biz.repository.NamespaceRepository
;
import
com.ctrip.apollo.biz.repository.NamespaceRepository
;
import
com.ctrip.apollo.biz.repository.ReleaseRepository
;
import
com.ctrip.apollo.biz.repository.ReleaseRepository
;
import
com.ctrip.apollo.core.exception.NotFoundException
;
import
com.ctrip.apollo.core.exception.NotFoundException
;
import
com.ctrip.apollo.core.utils.StringUtils
;
import
com.google.gson.Gson
;
import
com.google.gson.Gson
;
/**
/**
...
@@ -50,6 +51,9 @@ public class ReleaseService {
...
@@ -50,6 +51,9 @@ public class ReleaseService {
List
<
Item
>
items
=
itemRepository
.
findByNamespaceIdOrderByLineNumAsc
(
namespace
.
getId
());
List
<
Item
>
items
=
itemRepository
.
findByNamespaceIdOrderByLineNumAsc
(
namespace
.
getId
());
Map
<
String
,
String
>
configurations
=
new
HashMap
<
String
,
String
>();
Map
<
String
,
String
>
configurations
=
new
HashMap
<
String
,
String
>();
for
(
Item
item
:
items
)
{
for
(
Item
item
:
items
)
{
if
(
StringUtils
.
isEmpty
(
item
.
getKey
()))
{
continue
;
}
configurations
.
put
(
item
.
getKey
(),
item
.
getValue
());
configurations
.
put
(
item
.
getKey
(),
item
.
getValue
());
}
}
...
...
apollo-client/src/test/java/com/ctrip/apollo/integration/ConfigIntegrationTest.java
View file @
954a562e
package
com
.
ctrip
.
apollo
.
integration
;
package
com
.
ctrip
.
apollo
.
integration
;
import
com.google.common.collect.ImmutableMap
;
import
static
org
.
hamcrest
.
core
.
IsEqual
.
equalTo
;
import
com.google.common.collect.Lists
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
com.google.common.collect.Maps
;
import
static
org
.
junit
.
Assert
.
assertThat
;
import
com.ctrip.apollo.Config
;
import
com.ctrip.apollo.ConfigChangeListener
;
import
com.ctrip.apollo.ConfigService
;
import
com.ctrip.apollo.core.ConfigConsts
;
import
com.ctrip.apollo.core.dto.ApolloConfig
;
import
com.ctrip.apollo.core.utils.ClassLoaderUtil
;
import
com.ctrip.apollo.model.ConfigChangeEvent
;
import
org.eclipse.jetty.server.Request
;
import
org.eclipse.jetty.server.handler.AbstractHandler
;
import
org.eclipse.jetty.server.handler.ContextHandler
;
import
org.junit.After
;
import
org.junit.Before
;
import
org.junit.Test
;
import
java.io.File
;
import
java.io.File
;
import
java.io.FileOutputStream
;
import
java.io.FileOutputStream
;
import
java.io.IOException
;
import
java.io.IOException
;
import
java.nio.file.Files
;
import
java.util.List
;
import
java.util.List
;
import
java.util.Map
;
import
java.util.Map
;
import
java.util.Properties
;
import
java.util.Properties
;
...
@@ -31,9 +17,23 @@ import javax.servlet.ServletException;
...
@@ -31,9 +17,23 @@ import javax.servlet.ServletException;
import
javax.servlet.http.HttpServletRequest
;
import
javax.servlet.http.HttpServletRequest
;
import
javax.servlet.http.HttpServletResponse
;
import
javax.servlet.http.HttpServletResponse
;
import
static
org
.
hamcrest
.
core
.
IsEqual
.
equalTo
;
import
org.eclipse.jetty.server.Request
;
import
static
org
.
junit
.
Assert
.
assertEquals
;
import
org.eclipse.jetty.server.handler.AbstractHandler
;
import
static
org
.
junit
.
Assert
.
assertThat
;
import
org.eclipse.jetty.server.handler.ContextHandler
;
import
org.junit.After
;
import
org.junit.Before
;
import
org.junit.Test
;
import
com.ctrip.apollo.Config
;
import
com.ctrip.apollo.ConfigChangeListener
;
import
com.ctrip.apollo.ConfigService
;
import
com.ctrip.apollo.core.ConfigConsts
;
import
com.ctrip.apollo.core.dto.ApolloConfig
;
import
com.ctrip.apollo.core.utils.ClassLoaderUtil
;
import
com.ctrip.apollo.model.ConfigChangeEvent
;
import
com.google.common.collect.ImmutableMap
;
import
com.google.common.collect.Lists
;
import
com.google.common.collect.Maps
;
/**
/**
* @author Jason Song(song_s@ctrip.com)
* @author Jason Song(song_s@ctrip.com)
...
@@ -56,8 +56,8 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
...
@@ -56,8 +56,8 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
@Override
@Override
@After
@After
public
void
tearDown
()
throws
Exception
{
public
void
tearDown
()
throws
Exception
{
super
.
tearDown
();
recursiveDelete
(
configDir
);
recursiveDelete
(
configDir
);
super
.
tearDown
();
}
}
private
void
recursiveDelete
(
File
file
)
{
private
void
recursiveDelete
(
File
file
)
{
...
@@ -69,7 +69,12 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
...
@@ -69,7 +69,12 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
recursiveDelete
(
f
);
recursiveDelete
(
f
);
}
}
}
}
file
.
delete
();
try
{
Files
.
deleteIfExists
(
file
.
toPath
());
}
catch
(
IOException
e
)
{
e
.
printStackTrace
();
}
}
}
@Test
@Test
...
@@ -108,8 +113,7 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
...
@@ -108,8 +113,7 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
@Test
@Test
public
void
testGetConfigWithNoLocalFileAndRemoteConfigError
()
throws
Exception
{
public
void
testGetConfigWithNoLocalFileAndRemoteConfigError
()
throws
Exception
{
ContextHandler
ContextHandler
handler
=
handler
=
mockConfigServerHandler
(
HttpServletResponse
.
SC_INTERNAL_SERVER_ERROR
,
null
);
mockConfigServerHandler
(
HttpServletResponse
.
SC_INTERNAL_SERVER_ERROR
,
null
);
startServerWithHandlers
(
handler
);
startServerWithHandlers
(
handler
);
...
@@ -129,13 +133,11 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
...
@@ -129,13 +133,11 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
properties
.
put
(
someKey
,
someValue
);
properties
.
put
(
someKey
,
someValue
);
createLocalCachePropertyFile
(
properties
);
createLocalCachePropertyFile
(
properties
);
ContextHandler
ContextHandler
handler
=
handler
=
mockConfigServerHandler
(
HttpServletResponse
.
SC_INTERNAL_SERVER_ERROR
,
null
);
mockConfigServerHandler
(
HttpServletResponse
.
SC_INTERNAL_SERVER_ERROR
,
null
);
startServerWithHandlers
(
handler
);
startServerWithHandlers
(
handler
);
Config
config
=
ConfigService
.
getConfig
();
Config
config
=
ConfigService
.
getConfig
();
assertEquals
(
someValue
,
config
.
getProperty
(
someKey
,
null
));
assertEquals
(
someValue
,
config
.
getProperty
(
someKey
,
null
));
}
}
...
@@ -166,7 +168,7 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
...
@@ -166,7 +168,7 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
assertEquals
(
1
,
changeEvent
.
getChanges
().
size
());
assertEquals
(
1
,
changeEvent
.
getChanges
().
size
());
assertEquals
(
someValue
,
changeEvent
.
getChange
(
someKey
).
getOldValue
());
assertEquals
(
someValue
,
changeEvent
.
getChange
(
someKey
).
getOldValue
());
assertEquals
(
anotherValue
,
changeEvent
.
getChange
(
someKey
).
getNewValue
());
assertEquals
(
anotherValue
,
changeEvent
.
getChange
(
someKey
).
getNewValue
());
//if there is any assertion failed above, this line won't be executed
//
if there is any assertion failed above, this line won't be executed
changeEvents
.
add
(
changeEvent
);
changeEvents
.
add
(
changeEvent
);
}
}
});
});
...
@@ -186,7 +188,7 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
...
@@ -186,7 +188,7 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
context
.
setHandler
(
new
AbstractHandler
()
{
context
.
setHandler
(
new
AbstractHandler
()
{
@Override
@Override
public
void
handle
(
String
target
,
Request
baseRequest
,
HttpServletRequest
request
,
public
void
handle
(
String
target
,
Request
baseRequest
,
HttpServletRequest
request
,
HttpServletResponse
response
)
throws
IOException
,
ServletException
{
HttpServletResponse
response
)
throws
IOException
,
ServletException
{
response
.
setContentType
(
"application/json;charset=UTF-8"
);
response
.
setContentType
(
"application/json;charset=UTF-8"
);
response
.
setStatus
(
statusCode
);
response
.
setStatus
(
statusCode
);
...
@@ -210,12 +212,19 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
...
@@ -210,12 +212,19 @@ public class ConfigIntegrationTest extends BaseIntegrationTest {
private
File
createLocalCachePropertyFile
(
Properties
properties
)
throws
IOException
{
private
File
createLocalCachePropertyFile
(
Properties
properties
)
throws
IOException
{
File
file
=
new
File
(
configDir
,
assembleLocalCacheFileName
());
File
file
=
new
File
(
configDir
,
assembleLocalCacheFileName
());
properties
.
store
(
new
FileOutputStream
(
file
),
"Persisted by ConfigIntegrationTest"
);
FileOutputStream
in
=
null
;
try
{
in
=
new
FileOutputStream
(
file
);
properties
.
store
(
in
,
"Persisted by ConfigIntegrationTest"
);
}
finally
{
if
(
in
!=
null
)
{
in
.
close
();
}
}
return
file
;
return
file
;
}
}
private
String
assembleLocalCacheFileName
()
{
private
String
assembleLocalCacheFileName
()
{
return
String
.
format
(
"%s-%s-%s.properties"
,
someAppId
,
return
String
.
format
(
"%s-%s-%s.properties"
,
someAppId
,
someClusterName
,
someNamespace
);
someClusterName
,
someNamespace
);
}
}
}
}
apollo-portal/src/main/java/com/ctrip/apollo/portal/api/AdminServiceAPI.java
View file @
954a562e
...
@@ -76,7 +76,7 @@ public class AdminServiceAPI {
...
@@ -76,7 +76,7 @@ public class AdminServiceAPI {
public
List
<
ItemDTO
>
findItems
(
String
appId
,
Env
env
,
String
clusterName
,
String
namespace
)
{
public
List
<
ItemDTO
>
findItems
(
String
appId
,
Env
env
,
String
clusterName
,
String
namespace
)
{
if
(
StringUtils
.
isContainEmpty
(
appId
,
clusterName
,
namespace
))
{
if
(
StringUtils
.
isContainEmpty
(
appId
,
clusterName
,
namespace
))
{
return
Collections
.
EMPTY_LIST
;
return
Collections
.
emptyList
()
;
}
}
return
Arrays
.
asList
(
restTemplate
.
getForObject
(
getAdminServiceHost
(
env
)
+
String
return
Arrays
.
asList
(
restTemplate
.
getForObject
(
getAdminServiceHost
(
env
)
+
String
...
...
apollo-portal/src/main/java/com/ctrip/apollo/portal/controller/ConfigController.java
View file @
954a562e
...
@@ -11,7 +11,6 @@ import com.ctrip.apollo.portal.entity.form.NamespaceReleaseModel;
...
@@ -11,7 +11,6 @@ import com.ctrip.apollo.portal.entity.form.NamespaceReleaseModel;
import
com.ctrip.apollo.portal.service.ConfigService
;
import
com.ctrip.apollo.portal.service.ConfigService
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.http.ResponseEntity
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.PathVariable
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RequestBody
;
import
org.springframework.web.bind.annotation.RequestMapping
;
import
org.springframework.web.bind.annotation.RequestMapping
;
...
@@ -39,7 +38,7 @@ public class ConfigController {
...
@@ -39,7 +38,7 @@ public class ConfigController {
@RequestMapping
(
value
=
"/apps/{appId}/env/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items"
,
method
=
RequestMethod
.
PUT
,
consumes
=
{
@RequestMapping
(
value
=
"/apps/{appId}/env/{env}/clusters/{clusterName}/namespaces/{namespaceName}/items"
,
method
=
RequestMethod
.
PUT
,
consumes
=
{
"application/json"
})
"application/json"
})
public
ResponseEntity
modifyItems
(
@PathVariable
String
appId
,
@PathVariable
String
env
,
public
void
modifyItems
(
@PathVariable
String
appId
,
@PathVariable
String
env
,
@PathVariable
String
clusterName
,
@PathVariable
String
namespaceName
,
@PathVariable
String
clusterName
,
@PathVariable
String
namespaceName
,
@RequestBody
NamespaceTextModel
model
)
{
@RequestBody
NamespaceTextModel
model
)
{
...
@@ -56,7 +55,6 @@ public class ConfigController {
...
@@ -56,7 +55,6 @@ public class ConfigController {
}
}
configService
.
updateConfigItemByText
(
model
);
configService
.
updateConfigItemByText
(
model
);
return
ResponseEntity
.
ok
().
build
();
}
}
@RequestMapping
(
value
=
"/apps/{appId}/env/{env}/clusters/{clusterName}/namespaces/{namespaceName}/release"
,
method
=
RequestMethod
.
POST
,
consumes
=
{
@RequestMapping
(
value
=
"/apps/{appId}/env/{env}/clusters/{clusterName}/namespaces/{namespaceName}/release"
,
method
=
RequestMethod
.
POST
,
consumes
=
{
...
...
apollo-portal/src/main/java/com/ctrip/apollo/portal/service/ConfigService.java
View file @
954a562e
package
com
.
ctrip
.
apollo
.
portal
.
service
;
package
com
.
ctrip
.
apollo
.
portal
.
service
;
import
org.slf4j.Logger
;
import
org.slf4j.Logger
;
import
org.slf4j.LoggerFactory
;
import
org.slf4j.LoggerFactory
;
import
org.springframework.beans.factory.annotation.Autowired
;
import
org.springframework.beans.factory.annotation.Autowired
;
...
@@ -47,6 +46,7 @@ public class ConfigService {
...
@@ -47,6 +46,7 @@ public class ConfigService {
/**
/**
* load cluster all namespace info with items
* load cluster all namespace info with items
*
* @param appId
* @param appId
* @param env
* @param env
* @param clusterName
* @param clusterName
...
@@ -56,7 +56,7 @@ public class ConfigService {
...
@@ -56,7 +56,7 @@ public class ConfigService {
List
<
NamespaceDTO
>
namespaces
=
groupAPI
.
findGroupsByAppAndCluster
(
appId
,
env
,
clusterName
);
List
<
NamespaceDTO
>
namespaces
=
groupAPI
.
findGroupsByAppAndCluster
(
appId
,
env
,
clusterName
);
if
(
namespaces
==
null
||
namespaces
.
size
()
==
0
)
{
if
(
namespaces
==
null
||
namespaces
.
size
()
==
0
)
{
return
Collections
.
EMPTY_LIST
;
return
Collections
.
emptyList
()
;
}
}
List
<
NamespaceVO
>
namespaceVOs
=
new
LinkedList
<>();
List
<
NamespaceVO
>
namespaceVOs
=
new
LinkedList
<>();
...
@@ -67,8 +67,8 @@ public class ConfigService {
...
@@ -67,8 +67,8 @@ public class ConfigService {
namespaceVO
=
parseNamespace
(
appId
,
env
,
clusterName
,
namespace
);
namespaceVO
=
parseNamespace
(
appId
,
env
,
clusterName
,
namespace
);
namespaceVOs
.
add
(
namespaceVO
);
namespaceVOs
.
add
(
namespaceVO
);
}
catch
(
Exception
e
)
{
}
catch
(
Exception
e
)
{
logger
.
error
(
"parse namespace error. app id:{}, env:{}, clusterName:{}, namespace:{}"
,
appId
,
env
,
clusterName
,
logger
.
error
(
"parse namespace error. app id:{}, env:{}, clusterName:{}, namespace:{}"
,
namespace
.
getNamespaceName
(),
e
);
appId
,
env
,
clusterName
,
namespace
.
getNamespaceName
(),
e
);
return
namespaceVOs
;
return
namespaceVOs
;
}
}
}
}
...
@@ -76,8 +76,8 @@ public class ConfigService {
...
@@ -76,8 +76,8 @@ public class ConfigService {
return
namespaceVOs
;
return
namespaceVOs
;
}
}
@SuppressWarnings
(
"unchecked"
)
private
NamespaceVO
parseNamespace
(
String
appId
,
Env
env
,
String
clusterName
,
NamespaceDTO
namespace
)
{
private
NamespaceVO
parseNamespace
(
String
appId
,
Env
env
,
String
clusterName
,
NamespaceDTO
namespace
)
{
NamespaceVO
namespaceVO
=
new
NamespaceVO
();
NamespaceVO
namespaceVO
=
new
NamespaceVO
();
namespaceVO
.
setNamespace
(
namespace
);
namespaceVO
.
setNamespace
(
namespace
);
...
@@ -133,7 +133,8 @@ public class ConfigService {
...
@@ -133,7 +133,8 @@ public class ConfigService {
/**
/**
* parse config text and update config items
* parse config text and update config items
* @return parse result
*
* @return parse result
*/
*/
public
void
updateConfigItemByText
(
NamespaceTextModel
model
)
{
public
void
updateConfigItemByText
(
NamespaceTextModel
model
)
{
String
appId
=
model
.
getAppId
();
String
appId
=
model
.
getAppId
();
...
@@ -143,8 +144,8 @@ public class ConfigService {
...
@@ -143,8 +144,8 @@ public class ConfigService {
long
namespaceId
=
model
.
getNamespaceId
();
long
namespaceId
=
model
.
getNamespaceId
();
String
configText
=
model
.
getConfigText
();
String
configText
=
model
.
getConfigText
();
ItemChangeSets
changeSets
=
ItemChangeSets
changeSets
=
resolver
.
resolve
(
namespaceId
,
configText
,
resolver
.
resolve
(
namespaceId
,
configText
,
itemAPI
.
findItems
(
appId
,
env
,
clusterName
,
namespaceName
));
itemAPI
.
findItems
(
appId
,
env
,
clusterName
,
namespaceName
));
try
{
try
{
changeSets
.
setModifyBy
(
model
.
getModifyBy
());
changeSets
.
setModifyBy
(
model
.
getModifyBy
());
enrichChangeSetBaseInfo
(
changeSets
);
enrichChangeSetBaseInfo
(
changeSets
);
...
@@ -155,18 +156,19 @@ public class ConfigService {
...
@@ -155,18 +156,19 @@ public class ConfigService {
}
}
private
void
enrichChangeSetBaseInfo
(
ItemChangeSets
changeSets
){
private
void
enrichChangeSetBaseInfo
(
ItemChangeSets
changeSets
)
{
for
(
ItemDTO
item
:
changeSets
.
getCreateItems
())
{
for
(
ItemDTO
item
:
changeSets
.
getCreateItems
())
{
item
.
setDataChangeCreatedTime
(
new
Date
());
item
.
setDataChangeCreatedTime
(
new
Date
());
}
}
}
}
/**
/**
* createRelease config items
* createRelease config items
*
* @return
* @return
*/
*/
public
ReleaseDTO
createRelease
(
NamespaceReleaseModel
model
){
public
ReleaseDTO
createRelease
(
NamespaceReleaseModel
model
)
{
return
releaseAPI
.
release
(
model
.
getAppId
(),
model
.
getEnv
(),
model
.
getClusterName
(),
model
.
getNamespaceName
(),
return
releaseAPI
.
release
(
model
.
getAppId
(),
model
.
getEnv
(),
model
.
getClusterName
(),
model
.
getReleaseBy
(),
model
.
getReleaseComment
());
model
.
getNamespaceName
(),
model
.
getReleaseBy
(),
model
.
getReleaseComment
());
}
}
}
}
apollo-portal/src/main/java/com/ctrip/apollo/portal/service/txtresolver/PropertyResolver.java
View file @
954a562e
...
@@ -45,7 +45,7 @@ public class PropertyResolver implements ConfigTextResolver {
...
@@ -45,7 +45,7 @@ public class PropertyResolver implements ConfigTextResolver {
}
}
ItemChangeSets
changeSets
=
new
ItemChangeSets
();
ItemChangeSets
changeSets
=
new
ItemChangeSets
();
Map
<
Integer
,
String
>
newLineNumMapItem
=
new
HashMap
();
//use for delete blank and comment item
Map
<
Integer
,
String
>
newLineNumMapItem
=
new
HashMap
<
Integer
,
String
>
();
//use for delete blank and comment item
int
lineCounter
=
1
;
int
lineCounter
=
1
;
for
(
String
newItem
:
newItems
)
{
for
(
String
newItem
:
newItems
)
{
newItem
=
newItem
.
trim
();
newItem
=
newItem
.
trim
();
...
...
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