Browse Source

Add support for rendering Markdown commit messages (issue-203)

tags/v1.4.0
Bret K. Ikehara 11 years ago
parent
commit
cd9461c0c0

+ 8
- 0
src/main/distrib/data/gitblit.properties View File

# SINCE 0.8.0 # SINCE 0.8.0
web.repositoryListSwatches = true web.repositoryListSwatches = true
# Defines the default commit message renderer. This can be configured
# per-repository.
#
# Valid values are: plain, markdown
#
# SINCE 1.4.0
web.commitMessageRenderer = plain
# Choose the diff presentation style: gitblt, gitweb, or plain # Choose the diff presentation style: gitblt, gitweb, or plain
# #
# SINCE 0.5.0 # SINCE 0.5.0

+ 13
- 0
src/main/java/com/gitblit/Constants.java View File

return this == LOCAL; return this == LOCAL;
} }
} }
public static enum CommitMessageRenderer {
PLAIN, MARKDOWN;
public static CommitMessageRenderer fromName(String name) {
for (CommitMessageRenderer renderer : values()) {
if (renderer.name().equalsIgnoreCase(name)) {
return renderer;
}
}
return CommitMessageRenderer.PLAIN;
}
}
@Documented @Documented
@Retention(RetentionPolicy.RUNTIME) @Retention(RetentionPolicy.RUNTIME)

+ 64
- 5
src/main/java/com/gitblit/GitBlit.java View File

import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.security.Principal; import java.security.Principal;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import com.gitblit.Constants.AccountType; import com.gitblit.Constants.AccountType;
import com.gitblit.Constants.AuthenticationType; import com.gitblit.Constants.AuthenticationType;
import com.gitblit.Constants.AuthorizationControl; import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationRequest; import com.gitblit.Constants.FederationRequest;
import com.gitblit.Constants.FederationStrategy; import com.gitblit.Constants.FederationStrategy;
import com.gitblit.Constants.FederationToken; import com.gitblit.Constants.FederationToken;
import com.gitblit.utils.JGitUtils; import com.gitblit.utils.JGitUtils;
import com.gitblit.utils.JGitUtils.LastChange; import com.gitblit.utils.JGitUtils.LastChange;
import com.gitblit.utils.JsonUtils; import com.gitblit.utils.JsonUtils;
import com.gitblit.utils.MarkdownUtils;
import com.gitblit.utils.MetricUtils; import com.gitblit.utils.MetricUtils;
import com.gitblit.utils.ModelUtils; import com.gitblit.utils.ModelUtils;
import com.gitblit.utils.ObjectCache; import com.gitblit.utils.ObjectCache;
model.showReadme = getConfig(config, "showReadme", false); model.showReadme = getConfig(config, "showReadme", false);
model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false); model.skipSizeCalculation = getConfig(config, "skipSizeCalculation", false);
model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false); model.skipSummaryMetrics = getConfig(config, "skipSummaryMetrics", false);
model.commitMessageRenderer = CommitMessageRenderer.fromName(getConfig(config, "commitMessageRenderer",
settings.getString(Keys.web.commitMessageRenderer, null)));
model.federationStrategy = FederationStrategy.fromName(getConfig(config, model.federationStrategy = FederationStrategy.fromName(getConfig(config,
"federationStrategy", null)); "federationStrategy", null));
model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList( model.federationSets = new ArrayList<String>(Arrays.asList(config.getStringList(
Constants.CONFIG_GITBLIT, null, "indexBranch"))); Constants.CONFIG_GITBLIT, null, "indexBranch")));
model.metricAuthorExclusions = new ArrayList<String>(Arrays.asList(config.getStringList( model.metricAuthorExclusions = new ArrayList<String>(Arrays.asList(config.getStringList(
Constants.CONFIG_GITBLIT, null, "metricAuthorExclusions"))); Constants.CONFIG_GITBLIT, null, "metricAuthorExclusions")));
// Custom defined properties // Custom defined properties
model.customFields = new LinkedHashMap<String, String>(); model.customFields = new LinkedHashMap<String, String>();
for (String aProperty : config.getNames(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS)) { for (String aProperty : config.getNames(Constants.CONFIG_GITBLIT, Constants.CONFIG_CUSTOM_FIELDS)) {
config.setInt(Constants.CONFIG_GITBLIT, null, "maxActivityCommits", repository.maxActivityCommits); config.setInt(Constants.CONFIG_GITBLIT, null, "maxActivityCommits", repository.maxActivityCommits);
} }


CommitMessageRenderer defaultRenderer = CommitMessageRenderer.fromName(settings.getString(Keys.web.commitMessageRenderer, null));
if (repository.commitMessageRenderer == null || repository.commitMessageRenderer == defaultRenderer) {
// use default from config
config.unset(Constants.CONFIG_GITBLIT, null, "commitMessageRenderer");
} else {
// repository overrides default
config.setString(Constants.CONFIG_GITBLIT, null, "commitMessageRenderer",
repository.commitMessageRenderer.name());
}
updateList(config, "federationSets", repository.federationSets); updateList(config, "federationSets", repository.federationSets);
updateList(config, "preReceiveScript", repository.preReceiveScripts); updateList(config, "preReceiveScript", repository.preReceiveScripts);
updateList(config, "postReceiveScript", repository.postReceiveScripts); updateList(config, "postReceiveScript", repository.postReceiveScripts);
* Returns an html version of the commit message with any global or * Returns an html version of the commit message with any global or
* repository-specific regular expression substitution applied. * repository-specific regular expression substitution applied.
* *
* This method uses the preferred renderer to transform the commit message.
*
* @param repository
* @param text
* @return html version of the commit message
*/
public String processCommitMessage(RepositoryModel repository, String text) {
switch (repository.commitMessageRenderer) {
case MARKDOWN:
try {
String prepared = processCommitMessageRegex(repository.name, text);
return MarkdownUtils.transformMarkdown(prepared);
} catch (ParseException e) {
logger.error("Failed to render commit message as markdown", e);
}
break;
default:
// noop
break;
}
return processPlainCommitMessage(repository.name, text);
}
/**
* Returns an html version of the commit message with any global or
* repository-specific regular expression substitution applied.
*
* This method assumes the commit message is plain text.
*
* @param repositoryName * @param repositoryName
* @param text * @param text
* @return html version of the commit message * @return html version of the commit message
*/ */
public String processCommitMessage(String repositoryName, String text) {
String html = StringUtils.breakLinesForHtml(text);
public String processPlainCommitMessage(String repositoryName, String text) {
String html = StringUtils.escapeForHtml(text, false);
html = processCommitMessageRegex(repositoryName, html);
return StringUtils.breakLinesForHtml(html);
}
/**
* Apply globally or per-repository specified regex substitutions to the
* commit message.
*
* @param repositoryName
* @param text
* @return the processed commit message
*/
protected String processCommitMessageRegex(String repositoryName, String text) {
Map<String, String> map = new HashMap<String, String>(); Map<String, String> map = new HashMap<String, String>();
// global regex keys // global regex keys
if (settings.getBoolean(Keys.regex.global, false)) { if (settings.getBoolean(Keys.regex.global, false)) {
String definition = entry.getValue().trim(); String definition = entry.getValue().trim();
String[] chunks = definition.split("!!!"); String[] chunks = definition.split("!!!");
if (chunks.length == 2) { if (chunks.length == 2) {
html = html.replaceAll(chunks[0], chunks[1]);
text = text.replaceAll(chunks[0], chunks[1]);
} else { } else {
logger.warn(entry.getKey() logger.warn(entry.getKey()
+ " improperly formatted. Use !!! to separate match from replacement: " + " improperly formatted. Use !!! to separate match from replacement: "
+ definition); + definition);
} }
} }
return html;
return text;
} }


/** /**

+ 1
- 1
src/main/java/com/gitblit/SyndicationServlet.java View File

StringUtils.encodeURL(model.name.replace('/', fsc)), commit.getName()); StringUtils.encodeURL(model.name.replace('/', fsc)), commit.getName());
entry.published = commit.getCommitterIdent().getWhen(); entry.published = commit.getCommitterIdent().getWhen();
entry.contentType = "text/html"; entry.contentType = "text/html";
String message = GitBlit.self().processCommitMessage(model.name,
String message = GitBlit.self().processCommitMessage(model,
commit.getFullMessage()); commit.getFullMessage());
entry.content = message; entry.content = message;
entry.repository = model.name; entry.repository = model.name;

+ 2
- 0
src/main/java/com/gitblit/models/RepositoryModel.java View File

import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl; import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationStrategy; import com.gitblit.Constants.FederationStrategy;
import com.gitblit.utils.ArrayUtils; import com.gitblit.utils.ArrayUtils;
import com.gitblit.utils.ModelUtils; import com.gitblit.utils.ModelUtils;
public int gcPeriod; public int gcPeriod;
public int maxActivityCommits; public int maxActivityCommits;
public List<String> metricAuthorExclusions; public List<String> metricAuthorExclusions;
public CommitMessageRenderer commitMessageRenderer;
public transient boolean isCollectingGarbage; public transient boolean isCollectingGarbage;
public Date lastGC; public Date lastGC;

+ 2
- 1
src/main/java/com/gitblit/wicket/GitBlitWebApp.properties View File

gb.todaysActivityStats = today / {1} commits by {2} authors gb.todaysActivityStats = today / {1} commits by {2} authors
gb.todaysActivityNone = today / none gb.todaysActivityNone = today / none
gb.noActivityToday = there has been no activity today gb.noActivityToday = there has been no activity today
gb.anonymousUser= anonymous
gb.anonymousUser= anonymous
gb.commitMessageRenderer = commit message renderer

+ 1
- 1
src/main/java/com/gitblit/wicket/pages/CommitDiffPage.html View File

<div wicket:id="commitHeader">[commit header]</div> <div wicket:id="commitHeader">[commit header]</div>
<!-- full message --> <!-- full message -->
<pre style="border-style:none" "class="commit_message" wicket:id="fullMessage">[commit message]</pre>
<div wicket:id="fullMessage">[commit message]</div>
<!-- commit legend --> <!-- commit legend -->
<div class="hidden-phone" style="text-align:right;" wicket:id="commitLegend"></div> <div class="hidden-phone" style="text-align:right;" wicket:id="commitLegend"></div>

+ 1
- 1
src/main/java/com/gitblit/wicket/pages/CommitDiffPage.java View File



add(new CommitHeaderPanel("commitHeader", repositoryName, commit)); add(new CommitHeaderPanel("commitHeader", repositoryName, commit));


addFullText("fullMessage", commit.getFullMessage(), true);
addFullText("fullMessage", commit.getFullMessage());


// changed paths list // changed paths list
List<PathChangeModel> paths = JGitUtils.getFilesInCommit(r, commit); List<PathChangeModel> paths = JGitUtils.getFilesInCommit(r, commit);

+ 1
- 1
src/main/java/com/gitblit/wicket/pages/CommitPage.html View File

</div> </div>
<!-- full message --> <!-- full message -->
<pre class="commit_message" wicket:id="fullMessage">[commit message]</pre>
<div class="topborder" wicket:id="fullMessage">[commit message]</div>
<!-- git notes --> <!-- git notes -->
<table class="gitnotes"> <table class="gitnotes">

+ 3
- 3
src/main/java/com/gitblit/wicket/pages/CommitPage.java View File

}; };
add(parentsView); add(parentsView);
addFullText("fullMessage", c.getFullMessage(), true);
addFullText("fullMessage", c.getFullMessage());
// git notes // git notes
List<GitNote> notes = JGitUtils.getNotesOnCommit(r, c); List<GitNote> notes = JGitUtils.getNotesOnCommit(r, c);
item.add(new GravatarImage("noteAuthorAvatar", entry.notesRef.getAuthorIdent())); item.add(new GravatarImage("noteAuthorAvatar", entry.notesRef.getAuthorIdent()));
item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef item.add(WicketUtils.createTimestampLabel("authorDate", entry.notesRef
.getAuthorIdent().getWhen(), getTimeZone(), getTimeUtils())); .getAuthorIdent().getWhen(), getTimeZone(), getTimeUtils()));
item.add(new Label("noteContent", GitBlit.self().processCommitMessage(
repositoryName, entry.content)).setEscapeModelStrings(false));
item.add(new Label("noteContent", GitBlit.self().processPlainCommitMessage(repositoryName,
entry.content)).setEscapeModelStrings(false));
} }
}; };
add(notesView.setVisible(notes.size() > 0)); add(notesView.setVisible(notes.size() > 0));

+ 8
- 7
src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.html View File

<tr><th><wicket:message key="gb.skipSummaryMetrics"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="skipSummaryMetrics" tabindex="12" /> &nbsp;<span class="help-inline"><wicket:message key="gb.skipSummaryMetricsDescription"></wicket:message></span></label></td></tr> <tr><th><wicket:message key="gb.skipSummaryMetrics"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="skipSummaryMetrics" tabindex="12" /> &nbsp;<span class="help-inline"><wicket:message key="gb.skipSummaryMetricsDescription"></wicket:message></span></label></td></tr>
<tr><th><wicket:message key="gb.maxActivityCommits"></wicket:message></th><td class="edit"><select class="span2" wicket:id="maxActivityCommits" tabindex="13" /> &nbsp;<span class="help-inline"><wicket:message key="gb.maxActivityCommitsDescription"></wicket:message></span></td></tr> <tr><th><wicket:message key="gb.maxActivityCommits"></wicket:message></th><td class="edit"><select class="span2" wicket:id="maxActivityCommits" tabindex="13" /> &nbsp;<span class="help-inline"><wicket:message key="gb.maxActivityCommitsDescription"></wicket:message></span></td></tr>
<tr><th><wicket:message key="gb.metricAuthorExclusions"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="metricAuthorExclusions" size="40" tabindex="14" /></td></tr> <tr><th><wicket:message key="gb.metricAuthorExclusions"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="metricAuthorExclusions" size="40" tabindex="14" /></td></tr>
<tr><th><wicket:message key="gb.commitMessageRenderer"></wicket:message></th><td class="edit"><select class="span2" wicket:id="commitMessageRenderer" tabindex="15" /></td></tr>
<tr><th colspan="2"><hr/></th></tr> <tr><th colspan="2"><hr/></th></tr>
<tr><th><wicket:message key="gb.mailingLists"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="mailingLists" size="40" tabindex="15" /></td></tr>
<tr><th><wicket:message key="gb.mailingLists"></wicket:message></th><td class="edit"><input class="span8" type="text" wicket:id="mailingLists" size="40" tabindex="16" /></td></tr>
</tbody> </tbody>
</table> </table>
</div> </div>
<div class="tab-pane" id="permissions"> <div class="tab-pane" id="permissions">
<table class="plain"> <table class="plain">
<tbody class="settings"> <tbody class="settings">
<tr><th><wicket:message key="gb.owners"></wicket:message></th><td class="edit"><span wicket:id="owners" tabindex="16" /> </td></tr>
<tr><th><wicket:message key="gb.owners"></wicket:message></th><td class="edit"><span wicket:id="owners" tabindex="17" /> </td></tr>
<tr><th colspan="2"><hr/></th></tr> <tr><th colspan="2"><hr/></th></tr>
<tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span4" wicket:id="accessRestriction" tabindex="17" /></td></tr>
<tr><th><wicket:message key="gb.accessRestriction"></wicket:message></th><td class="edit"><select class="span4" wicket:id="accessRestriction" tabindex="18" /></td></tr>
<tr><th colspan="2"><hr/></th></tr> <tr><th colspan="2"><hr/></th></tr>
<tr><th><wicket:message key="gb.authorizationControl"></wicket:message></th><td style="padding:2px;"><span class="authorizationControl" wicket:id="authorizationControl"></span></td></tr> <tr><th><wicket:message key="gb.authorizationControl"></wicket:message></th><td style="padding:2px;"><span class="authorizationControl" wicket:id="authorizationControl"></span></td></tr>
<tr><th colspan="2"><hr/></th></tr> <tr><th colspan="2"><hr/></th></tr>
<tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="isFrozen" tabindex="18" /> &nbsp;<span class="help-inline"><wicket:message key="gb.isFrozenDescription"></wicket:message></span></label></td></tr>
<tr><th><wicket:message key="gb.allowForks"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="allowForks" tabindex="19" /> &nbsp;<span class="help-inline"><wicket:message key="gb.allowForksDescription"></wicket:message></span></label></td></tr>
<tr><th><wicket:message key="gb.verifyCommitter"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="verifyCommitter" tabindex="20" /> &nbsp;<span class="help-inline"><wicket:message key="gb.verifyCommitterDescription"></wicket:message></span><br/><span class="help-inline" style="padding-left:10px;"><wicket:message key="gb.verifyCommitterNote"></wicket:message></span></label></td></tr>
<tr><th><wicket:message key="gb.isFrozen"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="isFrozen" tabindex="19" /> &nbsp;<span class="help-inline"><wicket:message key="gb.isFrozenDescription"></wicket:message></span></label></td></tr>
<tr><th><wicket:message key="gb.allowForks"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="allowForks" tabindex="20" /> &nbsp;<span class="help-inline"><wicket:message key="gb.allowForksDescription"></wicket:message></span></label></td></tr>
<tr><th><wicket:message key="gb.verifyCommitter"></wicket:message></th><td class="edit"><label class="checkbox"><input type="checkbox" wicket:id="verifyCommitter" tabindex="21" /> &nbsp;<span class="help-inline"><wicket:message key="gb.verifyCommitterDescription"></wicket:message></span><br/><span class="help-inline" style="padding-left:10px;"><wicket:message key="gb.verifyCommitterNote"></wicket:message></span></label></td></tr>
<tr><th colspan="2"><hr/></th></tr> <tr><th colspan="2"><hr/></th></tr>
<tr><th><wicket:message key="gb.userPermissions"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr> <tr><th><wicket:message key="gb.userPermissions"></wicket:message></th><td style="padding:2px;"><span wicket:id="users"></span></td></tr>
<tr><th colspan="2"><hr/></th></tr> <tr><th colspan="2"><hr/></th></tr>
<div class="tab-pane" id="federation"> <div class="tab-pane" id="federation">
<table class="plain"> <table class="plain">
<tbody class="settings"> <tbody class="settings">
<tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="21" /></td></tr>
<tr><th><wicket:message key="gb.federationStrategy"></wicket:message></th><td class="edit"><select class="span4" wicket:id="federationStrategy" tabindex="22" /></td></tr>
<tr><th><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr> <tr><th><wicket:message key="gb.federationSets"></wicket:message></th><td style="padding:2px;"><span wicket:id="federationSets"></span></td></tr>
</tbody> </tbody>
</table> </table>

+ 5
- 1
src/main/java/com/gitblit/wicket/pages/EditRepositoryPage.java View File

import com.gitblit.Constants; import com.gitblit.Constants;
import com.gitblit.Constants.AccessRestrictionType; import com.gitblit.Constants.AccessRestrictionType;
import com.gitblit.Constants.AuthorizationControl; import com.gitblit.Constants.AuthorizationControl;
import com.gitblit.Constants.CommitMessageRenderer;
import com.gitblit.Constants.FederationStrategy; import com.gitblit.Constants.FederationStrategy;
import com.gitblit.Constants.RegistrantType; import com.gitblit.Constants.RegistrantType;
import com.gitblit.GitBlit; import com.gitblit.GitBlit;
} }
}); });
List<CommitMessageRenderer> renderers = Arrays.asList(CommitMessageRenderer.values());
DropDownChoice<CommitMessageRenderer> messageRendererChoice = new DropDownChoice<CommitMessageRenderer>("commitMessageRenderer", renderers);
form.add(messageRendererChoice);
form.add(new Button("save")); form.add(new Button("save"));
Button cancel = new Button("cancel") { Button cancel = new Button("cancel") {
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
return Integer.toString(index); return Integer.toString(index);
} }
} }
} }

+ 11
- 6
src/main/java/com/gitblit/wicket/pages/RepositoryPage.java View File

add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r, getRepositoryModel().showRemoteBranches))); add(new RefsPanel("refsPanel", repositoryName, c, JGitUtils.getAllRefs(r, getRepositoryModel().showRemoteBranches)));
} }
protected void addFullText(String wicketId, String text, boolean substituteRegex) {
String html = StringUtils.escapeForHtml(text, false);
if (substituteRegex) {
html = GitBlit.self().processCommitMessage(repositoryName, html);
} else {
html = StringUtils.breakLinesForHtml(html);
protected void addFullText(String wicketId, String text) {
RepositoryModel model = getRepositoryModel();
String content = GitBlit.self().processCommitMessage(model, text);
String html;
switch (model.commitMessageRenderer) {
case MARKDOWN:
html = MessageFormat.format("<div class='commit_message'>{0}</div>", content);
break;
default:
html = MessageFormat.format("<pre class='commit_message'>{0}</pre>", content);
break;
} }
add(new Label(wicketId, html).setEscapeModelStrings(false)); add(new Label(wicketId, html).setEscapeModelStrings(false));
} }

+ 1
- 1
src/main/java/com/gitblit/wicket/pages/TagPage.html View File

</div> </div>
<!-- full message --> <!-- full message -->
<pre class="commit_message" wicket:id="fullMessage">[commit message]</pre>
<div class="topborder" wicket:id="fullMessage">[commit message]</div>
<wicket:fragment wicket:id="fullPersonIdent"> <wicket:fragment wicket:id="fullPersonIdent">
<span wicket:id="personName"></span><span wicket:id="personAddress"></span> <span wicket:id="personName"></span><span wicket:id="personAddress"></span>

+ 1
- 1
src/main/java/com/gitblit/wicket/pages/TagPage.java View File

} }
add(WicketUtils.createTimestampLabel("tagDate", when, getTimeZone(), getTimeUtils())); add(WicketUtils.createTimestampLabel("tagDate", when, getTimeZone(), getTimeUtils()));
addFullText("fullMessage", tagRef.getFullMessage(), true);
addFullText("fullMessage", tagRef.getFullMessage());
} }
@Override @Override

+ 3
- 0
src/main/resources/gitblit.css View File

.commit_message { .commit_message {
padding: 8px; padding: 8px;
}
.topborder {
border: solid #ddd; border: solid #ddd;
border-width: 1px 0px 0px; border-width: 1px 0px 0px;
border-radius: 0px; border-radius: 0px;

Loading…
Cancel
Save