Merge "Mail footer discoverability helper"

This commit is contained in:
Wyatt Allen
2017-07-25 16:19:18 +00:00
committed by Gerrit Code Review
13 changed files with 452 additions and 22 deletions

View File

@@ -142,6 +142,108 @@ When sending email to an internal group, the internal group's read
access is automatically checked by Gerrit and therefore does not access is automatically checked by Gerrit and therefore does not
need to use the `visibleto:` operator in the filter. need to use the `visibleto:` operator in the filter.
[[footers]]
== Email Footers
Notification emails related to changes include metadata about the change
to support writing mail filters. This metadata is included in the form
of footers in the message content. For HTML emails, these footers are
hidden, but they can be examined by viewing the HTML source of messages.
In this way users may apply filters and rules to their incoming Gerrit
notifications using the values of these footers. For example a Gmail
filter to find emails regarding reviews that you are a reviewer of might
take the following form.
----
"Gerrit-Reviewer: Your Name <your.email@example.com>"
----
[[Gerrit-MessageType]]Gerrit-MessageType::
The message type footer states the type of the message and will take one
of the following values.
* abandon
* comment
* deleteReviewer
* deleteVote
* merged
* newchange
* newpatchset
* restore
* revert
* setassignee
[[Gerrit-Change-Id]]Gerrit-Change-Id::
The change ID footer states the ID of the change, such as
`I3443af49fcdc16ca941ee7cf2b5e33c1106f3b1d`.
[[Gerrit-Change-Number]]Gerrit-Change-Number::
The change number footer states the numeric ID of the change, for
example `92191`.
[[Gerrit-PatchSet]]Gerrit-PatchSet::
The patch set footer states the number of the patch set that the email
relates to. For example, a notification email for a vote being set on
the seventh patch set will take a value of `7`.
[[Gerrit-Owner]]Gerrit-Owner::
The owner footer states the name and email address of the change's
owner. For example, `Owner Name <owner@example.com>`.
[[Gerrit-Reviewer]]Gerrit-Reviewer::
The reviewer footers list the names and email addresses of the change's
reviewrs. One footer is included for each reviewer. For example, if a
change has two reviewers, the footers might include:
----
Gerrit-Reviewer: Reviewer One <one@example.com>
Gerrit-Reviewer: Reviewer Two <two@example.com>
----
[[Gerrit-CC]]Gerrit-CC::
The CC footers list the names and email addresses of those who have been
CC'd on the change. One footer is included for each reviewer. For
example, if a change CCs two users, the footers might include:
----
Gerrit-CC: User One <one@example.com>
Gerrit-CC: User Two <two@example.com>
----
[[Gerrit-Project]]Gerrit-Project::
The project footer states the project to which the change belongs.
[[Gerrit-Branch]]Gerrit-Branch::
The branch footer states the abbreviated name of the branch that the
change targets.
[[Gerrit-Comment-Date]]Gerrit-Comment-Date::
In comment emails, the comment date footer states the date that the
comment was posted.
[[Gerrit-HasComments]]Gerrit-HasComments::
In comment emails, the has-comments footer states whether inline
comments had been posted in that notification using "Yes" or "No", for
example `Gerrit-HasComments: Yes`.
[[Gerrit-HasLabels]]Gerrit-HasLabels::
In comment emails, the has-labels footer states whether label votes had
been posted in that notification using "Yes" or "No", for
example `Gerrit-HasLabels: No`.
GERRIT GERRIT
------ ------
Part of link:index.html[Gerrit Code Review] Part of link:index.html[Gerrit Code Review]

View File

@@ -239,6 +239,10 @@ public interface AccountConstants extends Constants {
String errorDialogTitleRegisterNewEmail(); String errorDialogTitleRegisterNewEmail();
String emailFilterHelpTitle();
String emailFilterHelp();
String newAgreement(); String newAgreement();
String agreementName(); String agreementName();

View File

@@ -158,7 +158,74 @@ descRegisterNewEmail = \
<p>A confirmation link will be sent by email to this address.</p>\ <p>A confirmation link will be sent by email to this address.</p>\
<p>You must click on the link to complete the registration and make the address available for selection.</p> <p>You must click on the link to complete the registration and make the address available for selection.</p>
errorDialogTitleRegisterNewEmail = Email Registration Failed errorDialogTitleRegisterNewEmail = Email Registration Failed
emailFilterHelpTitle = Mail Filters
emailFilterHelp = \
<p>\
Gerrit emails include metadata about the change to support \
writing mail filters.\
</p>\
<p>\
Here are some example Gmail queries that can be used for filters or \
for searching through archived messages. View the \
<a href="https://gerrit-review.googlesource.com/Documentation/user-notify.html"\
target="_blank" rel="nofollow">Gerrit documentation</a> for \
the complete set of footers.\
</p>\
<table>\
<tbody>\
<tr><th>Name</th><th>Query</th></tr>\
<tr>\
<td>Changes requesting my review</td>\
<td>\
<code>\
"Gerrit-Reviewer: <em>Your Name</em>\
&lt;<em>your.email@example.com</em>&gt;"\
</code>\
</td>\
</tr>\
<tr>\
<td>Changes from a specific owner</td>\
<td>\
<code>\
"Gerrit-Owner: <em>Owner name</em>\
&lt;<em>owner.email@example.com</em>&gt;"\
</code>\
</td>\
</tr>\
<tr>\
<td>Changes targeting a specific branch</td>\
<td>\
<code>\
"Gerrit-Branch: <em>branch-name</em>"\
</code>\
</td>\
</tr>\
<tr>\
<td>Changes in a specific project</td>\
<td>\
<code>\
"Gerrit-Project: <em>project-name</em>"\
</code>\
</td>\
</tr>\
<tr>\
<td>Messages related to a specific Change ID</td>\
<td>\
<code>\
"Gerrit-Change-Id: <em>Change ID</em>"\
</code>\
</td>\
</tr>\
<tr>\
<td>Messages related to a specific change number</td>\
<td>\
<code>\
"Gerrit-Change-Number: <em>change number</em>"\
</code>\
</td>\
</tr>\
</tbody>\
</table>
newAgreement = New Contributor Agreement newAgreement = New Contributor Agreement
agreementName = Name agreementName = Name

View File

@@ -21,6 +21,7 @@ import com.google.gerrit.client.rpc.CallbackGroup;
import com.google.gerrit.client.rpc.GerritCallback; import com.google.gerrit.client.rpc.GerritCallback;
import com.google.gerrit.client.rpc.NativeString; import com.google.gerrit.client.rpc.NativeString;
import com.google.gerrit.client.rpc.Natives; import com.google.gerrit.client.rpc.Natives;
import com.google.gerrit.client.ui.ComplexDisclosurePanel;
import com.google.gerrit.client.ui.OnEditEnabler; import com.google.gerrit.client.ui.OnEditEnabler;
import com.google.gerrit.common.PageLinks; import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.errors.EmailException; import com.google.gerrit.common.errors.EmailException;
@@ -153,6 +154,11 @@ class ContactPanelShort extends Composite {
} }
}); });
final ComplexDisclosurePanel mailFilterHelp =
new ComplexDisclosurePanel(Util.C.emailFilterHelpTitle(), false);
mailFilterHelp.setContent(new HTML(Util.C.emailFilterHelp()));
body.add(mailFilterHelp);
emailPick.addChangeHandler( emailPick.addChangeHandler(
new ChangeHandler() { new ChangeHandler() {
@Override @Override

View File

@@ -30,7 +30,8 @@
{/if} {/if}
{if $email.settingsUrl} {if $email.settingsUrl}
To unsubscribe, visit {$email.settingsUrl}{\n} To unsubscribe, or for help writing mail filters,{sp}
visit {$email.settingsUrl}{\n}
{/if} {/if}
{if $email.changeUrl or $email.settingsUrl} {if $email.changeUrl or $email.settingsUrl}

View File

@@ -29,7 +29,8 @@
{/if} {/if}
{if $email.changeUrl and $email.settingsUrl}{sp}{/if} {if $email.changeUrl and $email.settingsUrl}{sp}{/if}
{if $email.settingsUrl} {if $email.settingsUrl}
To unsubscribe, visit <a href="{$email.settingsUrl}">settings</a>. To unsubscribe, or for help writing mail filters,{sp}
visit <a href="{$email.settingsUrl}">settings</a>.
{/if} {/if}
</p> </p>
{/if} {/if}

View File

@@ -0,0 +1,63 @@
<!--
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<link rel="import" href="../base-url-behavior/base-url-behavior.html">
<script>
(function(window) {
'use strict';
const PROBE_PATH = '/Documentation/index.html';
const DOCS_BASE_PATH = '/Documentation';
let cachedPromise;
/** @polymerBehavior Gerrit.DocsUrlBehavior */
const DocsUrlBehavior = {
/**
* Get the docs base URL from either the server config or by probing.
* @param {Object} config The server config.
* @param {!Object} restApi A REST API instance
* @return {!Promise<String>} A promise that resolves with the docs base
* URL.
*/
getDocsBaseUrl(config, restApi) {
if (!cachedPromise) {
cachedPromise = new Promise(resolve => {
if (config && config.gerrit && config.gerrit.doc_url) {
resolve(config.gerrit.doc_url);
} else {
restApi.probePath(this.getBaseUrl() + PROBE_PATH).then(ok => {
resolve(ok ? (this.getBaseUrl() + DOCS_BASE_PATH) : null);
});
}
});
}
return cachedPromise;
},
/** For testing only. */
_clearDocsBaseUrlCache() {
cachedPromise = undefined;
},
};
window.Gerrit = window.Gerrit || {};
window.Gerrit.DocsUrlBehavior = [
window.Gerrit.BaseUrlBehavior,
DocsUrlBehavior,
];
})(window);
</script>

View File

@@ -0,0 +1,98 @@
<!--
Copyright (C) 2017 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Polymer included for the html import polyfill. -->
<script src="../../bower_components/webcomponentsjs/webcomponents.min.js"></script>
<script src="../../bower_components/web-component-tester/browser.js"></script>
<link rel="import" href="../../test/common-test-setup.html"/>
<title>docs-url-behavior</title>
<link rel="import" href="docs-url-behavior.html">
<script>void(0);</script>
<test-fixture id="basic">
<template>
<docs-url-behavior-element></docs-url-behavior-element>
</template>
</test-fixture>
<script>
suite('docs-url-behavior tests', () => {
let element;
suiteSetup(() => {
// Define a Polymer element that uses this behavior.
Polymer({
is: 'docs-url-behavior-element',
behaviors: [Gerrit.DocsUrlBehavior],
});
});
setup(() => {
element = fixture('basic');
element._clearDocsBaseUrlCache();
});
test('null config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
return element.getDocsBaseUrl(null, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.equal(docsBaseUrl, '/Documentation');
});
});
test('no doc config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
const config = {gerrit: {}};
return element.getDocsBaseUrl(config, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.equal(docsBaseUrl, '/Documentation');
});
});
test('has doc config', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(true)),
};
const config = {gerrit: {doc_url: 'foobar'}};
return element.getDocsBaseUrl(config, mockRestApi)
.then(docsBaseUrl => {
assert.isFalse(mockRestApi.probePath.called);
assert.equal(docsBaseUrl, 'foobar');
});
});
test('no probe', () => {
const mockRestApi = {
probePath: sinon.stub().returns(Promise.resolve(false)),
};
return element.getDocsBaseUrl(null, mockRestApi)
.then(docsBaseUrl => {
assert.isTrue(
mockRestApi.probePath.calledWith('/Documentation/index.html'));
assert.isNotOk(docsBaseUrl);
});
});
});
</script>

View File

@@ -15,6 +15,7 @@ limitations under the License.
--> -->
<link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
<link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html"> <link rel="import" href="../../../behaviors/base-url-behavior/base-url-behavior.html">
<link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html"> <link rel="import" href="../../shared/gr-dropdown/gr-dropdown.html">
<link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html"> <link rel="import" href="../../shared/gr-rest-api-interface/gr-rest-api-interface.html">

View File

@@ -103,6 +103,7 @@
behaviors: [ behaviors: [
Gerrit.BaseUrlBehavior, Gerrit.BaseUrlBehavior,
Gerrit.DocsUrlBehavior,
], ],
observers: [ observers: [
@@ -188,24 +189,9 @@
}, },
_loadConfig() { _loadConfig() {
this.$.restAPI.getConfig().then(config => { this.$.restAPI.getConfig()
if (config && config.gerrit && config.gerrit.doc_url) { .then(config => this.getDocsBaseUrl(config, this.$.restAPI))
this._docBaseUrl = config.gerrit.doc_url; .then(docBaseUrl => { this._docBaseUrl = docBaseUrl; });
}
if (!this._docBaseUrl) {
return this._probeDocLink('/Documentation/index.html');
}
});
},
_probeDocLink(path) {
return this.$.restAPI.probePath(this.getBaseUrl() + path).then(ok => {
if (ok) {
this._docBaseUrl = this.getBaseUrl() + '/Documentation';
} else {
this._docBaseUrl = null;
}
});
}, },
_accountLoaded(account) { _accountLoaded(account) {

View File

@@ -16,6 +16,7 @@ limitations under the License.
<link rel="import" href="../../../bower_components/polymer/polymer.html"> <link rel="import" href="../../../bower_components/polymer/polymer.html">
<link rel="import" href="../../../behaviors/docs-url-behavior/docs-url-behavior.html">
<link rel="import" href="../../../styles/shared-styles.html"> <link rel="import" href="../../../styles/shared-styles.html">
<link rel="import" href="../../../styles/gr-menu-page-styles.html"> <link rel="import" href="../../../styles/gr-menu-page-styles.html">
<link rel="import" href="../../../styles/gr-page-nav-styles.html"> <link rel="import" href="../../../styles/gr-page-nav-styles.html">
@@ -44,6 +45,12 @@ limitations under the License.
#email { #email {
margin-bottom: 1em; margin-bottom: 1em;
} }
.filters p {
margin-bottom: 1em;
}
.queryExample em {
color: violet;
}
</style> </style>
<style include="gr-form-styles"></style> <style include="gr-form-styles"></style>
<style include="gr-menu-page-styles"></style> <style include="gr-menu-page-styles"></style>
@@ -69,6 +76,7 @@ limitations under the License.
<a href="#Agreements">Agreements</a> <a href="#Agreements">Agreements</a>
</li> </li>
</template> </template>
<li><a href="#MailFilters">Mail Filters</a></li>
</ul> </ul>
</gr-page-nav> </gr-page-nav>
<main class="gr-form-styles"> <main class="gr-form-styles">
@@ -383,6 +391,76 @@ limitations under the License.
<gr-agreements-list id="agreementsList"></gr-agreements-list> <gr-agreements-list id="agreementsList"></gr-agreements-list>
</fieldset> </fieldset>
</template> </template>
<h2 id="MailFilters">Mail Filters</h2>
<fieldset class="filters">
<p>
Gerrit emails include metadata about the change to support
writing mail filters.
</p>
<p>
Here are some example Gmail queries that can be used for filters or
for searching through archived messages. View the
<a href$="[[_getFilterDocsLink(_docsBaseUrl)]]"
target="_blank"
rel="nofollow">Gerrit documentation</a>
for the complete set of footers.
</p>
<table>
<tbody>
<tr><th>Name</th><th>Query</th></tr>
<tr>
<td>Changes requesting my review</td>
<td>
<code class="queryExample">
"Gerrit-Reviewer: <em>Your Name</em>
&lt;<em>your.email@example.com</em>&gt;"
</code>
</td>
</tr>
<tr>
<td>Changes from a specific owner</td>
<td>
<code class="queryExample">
"Gerrit-Owner: <em>Owner name</em>
&lt;<em>owner.email@example.com</em>&gt;"
</code>
</td>
</tr>
<tr>
<td>Changes targeting a specific branch</td>
<td>
<code class="queryExample">
"Gerrit-Branch: <em>branch-name</em>"
</code>
</td>
</tr>
<tr>
<td>Changes in a specific project</td>
<td>
<code class="queryExample">
"Gerrit-Project: <em>project-name</em>"
</code>
</td>
</tr>
<tr>
<td>Messages related to a specific Change ID</td>
<td>
<code class="queryExample">
"Gerrit-Change-Id: <em>Change ID</em>"
</code>
</td>
</tr>
<tr>
<td>Messages related to a specific change number</td>
<td>
<code class="queryExample">
"Gerrit-Change-Number: <em>change number</em>"
</code>
</td>
</tr>
</tbody>
</table>
</fieldset>
</main> </main>
</div> </div>
<gr-rest-api-interface id="restAPI"></gr-rest-api-interface> <gr-rest-api-interface id="restAPI"></gr-rest-api-interface>

View File

@@ -25,6 +25,10 @@
'email_format', 'email_format',
]; ];
const GERRIT_DOCS_BASE_URL = 'https://gerrit-review.googlesource.com/' +
'Documentation';
const GERRIT_DOCS_FILTER_PATH = '/user-notify.html';
Polymer({ Polymer({
is: 'gr-settings-view', is: 'gr-settings-view',
@@ -103,6 +107,7 @@
value: null, value: null,
}, },
_serverConfig: Object, _serverConfig: Object,
_docsBaseUrl: String,
/** /**
* For testing purposes. * For testing purposes.
@@ -111,6 +116,7 @@
}, },
behaviors: [ behaviors: [
Gerrit.DocsUrlBehavior,
Gerrit.ChangeTableBehavior, Gerrit.ChangeTableBehavior,
], ],
@@ -144,9 +150,17 @@
promises.push(this.$.restAPI.getConfig().then(config => { promises.push(this.$.restAPI.getConfig().then(config => {
this._serverConfig = config; this._serverConfig = config;
const configPromises = [];
if (this._serverConfig.sshd) { if (this._serverConfig.sshd) {
return this.$.sshEditor.loadData(); configPromises.push(this.$.sshEditor.loadData());
} }
configPromises.push(
this.getDocsBaseUrl(config, this.$.restAPI)
.then(baseUrl => { this._docsBaseUrl = baseUrl; }));
return Promise.all(configPromises);
})); }));
if (this.params.emailToken) { if (this.params.emailToken) {
@@ -339,5 +353,13 @@
this._newEmail = ''; this._newEmail = '';
}); });
}, },
_getFilterDocsLink(docsBaseUrl) {
let base = docsBaseUrl;
if (!base || !base.startsWith('http')) {
base = GERRIT_DOCS_BASE_URL;
}
return base + GERRIT_DOCS_FILTER_PATH;
},
}); });
})(); })();

View File

@@ -141,6 +141,7 @@ limitations under the License.
// Behaviors tests. // Behaviors tests.
const behaviors = [ const behaviors = [
'base-url-behavior/base-url-behavior_test.html', 'base-url-behavior/base-url-behavior_test.html',
'docs-url-behavior/docs-url-behavior_test.html',
'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html', 'keyboard-shortcut-behavior/keyboard-shortcut-behavior_test.html',
'rest-client-behavior/rest-client-behavior_test.html', 'rest-client-behavior/rest-client-behavior_test.html',
'gr-change-table-behavior/gr-change-table-behavior_test.html', 'gr-change-table-behavior/gr-change-table-behavior_test.html',