Merge "Add ProjectCache.remove()"
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index f79b8c0..468e767 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -5,35 +5,57 @@
JVM as gerrit. It has full access to all gerrit internals. Plugins
are coupled to a specific major.minor gerrit version.
-REQUIREMENTS
+Requirements
------------
-To start development, you may download the sample maven project, which downloads
-the following dependencies;
+To start development, download the sample maven project, which downloads the
+following dependencies:
-* gerrit-sdk.jar file that matches the war file you are developing against
+* gerrit-sdk.jar file that matches the war file to develop against
Manifest
--------
-Plugins need to include the following data in the jar manifest file;
-Gerrit-Plugin = plugin_name
-Gerrit-Module = pkg.class
+Plugins need to include the following data in the jar manifest file:
+
+ Gerrit-Module = pkg.class
+
+Optionally include:
+
+ Gerrit-ReloadMode = 'reload' (default) or 'restart'
+
+If the plugin holds an exclusive resource that must be released before loading
+the plugin again, ReloadMode must be set to 'restart'. Otherwise 'reload' is
+sufficient.
SSH Commands
------------
-You may develop plugins which provide commands that can be accessed through the SSH interface.
-These commands register themselves as a part of SSH Commands (link).
+Plugins may provide commands that can be accessed through the SSH interface.
+These commands register themselves as a part of link:cmd-index.html[SSH Commands].
-Each of your plugins commands needs to extend BaseCommand.
+Each of the plugin commands needs to extend SshCommand.
-Any plugin which implements at least one ssh command needs to also provide a class which extends
-the PluginCommandModule in order to register the ssh command(s) in its configure method which you
-must override.
+Any plugin which implements at least one ssh command needs to also provide a
+class which extends the PluginCommandModule in order to register the ssh
+command(s) in its configure method which must be overriden.
-Registering is done by calling the command(String commandName).to(ClassName<? extends BaseCommand> klass)
+Registering is done by calling:
+
+ command(String commandName).to(ClassName<? extends SshCommand> klass)
+
+Documentation
+-------------
+
+Place files into Documentation/ or static/ and package them into the plugin jar
+to access them in a browser via <canonicalWebURL>/plugins/<pluginName>/...
+
+Deployment
+----------
+
+Deploy plugins into <review_site>/plugins/. The file name in that directory will
+be the plugin name on the server.
GERRIT
------
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 7647aaf..77904e5 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -19,6 +19,7 @@
* link:access-control.html[Access Controls]
* link:error-messages.html[Error Messages]
* link:rest-api.html[REST API]
+* link:user-notify.html[Subscribing to Email Notifications]
* link:user-submodules.html[Subscribing to Git Submodules]
Installation
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 69018d8..e50979a 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -52,6 +52,7 @@
|JSR 305 | <<jsr305,New-Style BSD>>
|dk.brics.automaton | <<automaton,New-Style BSD>>
|Java Concurrency in Practice Annotations | <<jcip,Create Commons Attribution License>>
+|pegdown | <<apache2,Apache License 2.0>>
|======================================================================
Cryptography Notice
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
new file mode 100644
index 0000000..ae3c2d09
--- /dev/null
+++ b/Documentation/user-notify.txt
@@ -0,0 +1,127 @@
+Gerrit Code Review - Email Notifications
+========================================
+
+Description
+-----------
+
+Gerrit can automatically notify users by email when new changes are
+uploaded for review, after comments have been posted on a change,
+or after the change has been submitted to a branch.
+
+User Level Settings
+-------------------
+
+Individual users can configure email subscriptions by editing
+watched projects through Settings > Watched Projects with the web UI.
+
+Specific projects may be watched, or the special project
+`All-Projects` can be watched to watch all projects that
+are visible to the user.
+
+Change search expressions can be used to filter change notifications
+to specific subsets, for example `branch:master` to only see changes
+proposed for the master branch.
+
+Project Level Settings
+----------------------
+
+Project owners and site administrators can configure project level
+notifications, enabling Gerrit Code Review to automatically send
+emails to team mailing lists, or groups of users. Project settings
+are stored inside of the `refs/meta/config` branch of each Git
+repository, and are placed inside of the `project.config` file.
+
+To edit the project level notify settings, ensure the project owner
+has Push permission already granted for the `refs/meta/config`
+branch. Consult link:access-control.html[access controls] for
+details on how access permissions work.
+
+Initialize a temporary Git repository to edit the configuration:
+====
+ mkdir cfg_dir
+ cd cfg_dir
+ git init
+====
+
+Download the existing configuration from Gerrit:
+====
+ git fetch ssh://localhost:29418/project refs/meta/config
+ git checkout FETCH_HEAD
+====
+
+Enable notifications to an email address by adding to
+`project.config`, this can be done using the `git config` command:
+====
+ git config -f project.config --add notify.team.email team-address@example.com
+ git config -f project.config --add notify.team.email paranoid-manager@example.com
+====
+
+Examining the project.config file with any text editor should show
+a new notify section describing the email addresses to deliver to:
+----
+ [notify "team"]
+ email = team-address@example.com
+ email = paranoid-manager@example.com
+----
+
+Each notify section within a single project.config file must have a
+unique name. The section name itself does not matter and may later
+appear in the web UI. Naming a section after the email address or
+group it delivers to is typical. Multiple sections can be specified
+if different filters are needed.
+
+Commit the configuration change, and push it back:
+====
+ git commit -a -m "Notify team-address@example.com of changes"
+ git push ssh://localhost:29418/project HEAD:refs/meta/config
+====
+
+[[notify.name.email]]notify.<name>.email::
++
+List of email addresses to send matching notifications to. Each
+email address should be placed on its own line.
++
+Internal groups within Gerrit Code Review can also be named using
+`group NAME` syntax. If this format is used the group's UUID must
+also appear in the corresponding `groups` file. Gerrit will expand
+the group membership and BCC all current users.
+
+[[notify.name.type]]notify.<name>.type::
++
+Types of notifications to send. If not specified, all notifications
+are sent.
++
+* `new_changes`: Only newly created changes.
+* `all_comments`: Only comments on existing changes.
+* `submitted_changes`: Only changes that have been submitted.
+* `all`: All notifications.
+
++
+Like email, this variable may be a list of options.
+
+[[notify.name.filter]]notify.<name>.filter::
++
+link:user-search.html[Change search expression] to match changes that
+should be sent to the emails named in this section. Within a Git-style
+configuration file double quotes around complex operator values may
+need to be escaped, e.g. `filter = branch:\"^(maint|stable)-.*\"`.
+
+When sending email to a bare email address in a notify block, Gerrit
+Code Review ignores read access controls and assumes the administrator
+has set the filtering options correctly. Project owners can implement
+security filtering by adding the `visibleto:groupname` predicate to
+the filter expression, for example:
+
+====
+ [notify "Developers"]
+ email = team-address@example.com
+ filter = visibleto:Developers
+====
+
+When sending email to an internal group, the internal group's read
+access is automatically checked by Gerrit and therefore does not
+need to use the `visibleto:` operator in the filter.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
index f114afd..8cac117 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -147,7 +147,7 @@
public boolean equals(Object other) {
if (other instanceof NamePair) {
NamePair np = (NamePair) other;
- return pluginName.equals(np) && exportName.equals(np);
+ return pluginName.equals(np.pluginName) && exportName.equals(np.exportName);
}
return false;
}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index b5cca86..4c0b1ba 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -17,8 +17,8 @@
import com.google.gerrit.client.Dispatcher;
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.client.ui.FancyFlexTable;
import com.google.gerrit.client.ui.Hyperlink;
@@ -286,7 +286,7 @@
CheckBox checkBox = new CheckBox();
table.setWidget(row, 1, checkBox);
checkBox.setEnabled(enabled);
- table.setWidget(row, 2, AccountDashboardLink.link(accounts, accountId));
+ table.setWidget(row, 2, AccountLink.link(accounts, accountId));
table.setText(row, 3, accounts.get(accountId).getPreferredEmail());
final FlexCellFormatter fmt = table.getFlexCellFormatter();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
index 09716cc..c0c9ce8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -21,7 +21,7 @@
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.AddMemberBox;
import com.google.gerrit.client.ui.ReviewerSuggestOracle;
import com.google.gerrit.common.data.AccountInfoCache;
@@ -129,8 +129,8 @@
accountCache = aic;
}
- private AccountDashboardLink link(final Account.Id id) {
- return AccountDashboardLink.link(accountCache, id);
+ private AccountLink link(final Account.Id id) {
+ return AccountLink.link(accountCache, id);
}
void display(ChangeDetail detail) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index f8373cc..865e389 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -17,7 +17,7 @@
import static com.google.gerrit.client.FormatUtil.mediumFormat;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.ProjectLink;
@@ -92,7 +92,7 @@
changeIdLabel.setPreviewText(chg.getKey().get());
table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
- table.setWidget(R_OWNER, 1, AccountDashboardLink.link(acc, chg.getOwner()));
+ table.setWidget(R_OWNER, 1, AccountLink.link(acc, chg.getOwner()));
table.setWidget(R_PROJECT, 1, new ProjectLink(chg.getProject(), chg.getStatus()));
table.setWidget(R_BRANCH, 1, new BranchLink(dst.getShortName(), chg
.getProject(), chg.getStatus(), dst.get(), null));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 19a770e..44a49a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -20,7 +20,7 @@
import com.google.gerrit.client.Gerrit;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
import com.google.gerrit.client.ui.BranchLink;
import com.google.gerrit.client.ui.ChangeLink;
import com.google.gerrit.client.ui.NavigationTable;
@@ -226,8 +226,8 @@
setRowItem(row, c);
}
- private AccountDashboardLink link(final Account.Id id) {
- return AccountDashboardLink.link(accountCache, id);
+ private AccountLink link(final Account.Id id) {
+ return AccountLink.link(accountCache, id);
}
public void addSection(final Section s) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
index 68436ca..8b86d50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetComplexDisclosurePanel.java
@@ -20,9 +20,9 @@
import com.google.gerrit.client.GitwebLink;
import com.google.gerrit.client.patches.PatchUtil;
import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
import com.google.gerrit.client.ui.CommentedActionDialog;
import com.google.gerrit.client.ui.ComplexDisclosurePanel;
+import com.google.gerrit.client.ui.InlineHyperlink;
import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.ChangeDetail;
@@ -383,7 +383,8 @@
if (who.getName() != null) {
final Account.Id aId = who.getAccount();
if (aId != null) {
- fp.add(new AccountDashboardLink(who.getName(), aId));
+ fp.add(new InlineHyperlink(who.getName(), PageLinks.toAccountQuery(who
+ .getName())));
} else {
final InlineLabel lbl = new InlineLabel(who.getName());
lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
similarity index 60%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
index 5233a6b..a4f4509 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2008 The Android Open Source Project
+// Copyright (C) 2012 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.
@@ -16,41 +16,39 @@
import com.google.gerrit.client.FormatUtil;
import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.AccountDashboardScreen;
+import com.google.gerrit.client.changes.QueryScreen;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccountInfo;
import com.google.gerrit.common.data.AccountInfoCache;
import com.google.gerrit.reviewdb.client.Account;
/** Link to any user's account dashboard. */
-public class AccountDashboardLink extends InlineHyperlink {
+public class AccountLink extends InlineHyperlink {
/** Create a link after locating account details from an active cache. */
- public static AccountDashboardLink link(final AccountInfoCache cache,
+ public static AccountLink link(final AccountInfoCache cache,
final Account.Id id) {
final AccountInfo ai = cache.get(id);
- return ai != null ? new AccountDashboardLink(ai) : null;
+ return ai != null ? new AccountLink(ai) : null;
}
- private Account.Id accountId;
+ private final String query;
- public AccountDashboardLink(final AccountInfo ai) {
+ public AccountLink(final AccountInfo ai) {
this(FormatUtil.name(ai), ai);
}
- public AccountDashboardLink(final String text, final AccountInfo ai) {
- this(text, ai.getId());
+ public AccountLink(final String text, final AccountInfo ai) {
+ super(text, PageLinks.toAccountQuery(FormatUtil.name(ai)));
setTitle(FormatUtil.nameEmail(ai));
+ this.query = "owner:\"" + FormatUtil.name(ai) + "\"";
}
- public AccountDashboardLink(final String text, final Account.Id ai) {
- super(text, PageLinks.toAccountDashboard(ai));
- addStyleName(Gerrit.RESOURCES.css().accountName());
- accountId = ai;
+ private Screen createScreen() {
+ return QueryScreen.forQuery(query);
}
@Override
public void go() {
- Gerrit.display(getTargetHistoryToken(), //
- new AccountDashboardScreen(accountId));
+ Gerrit.display(getTargetHistoryToken(), createScreen());
}
}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index 23dbaac..a6b9429 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -18,6 +18,7 @@
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.server.documentation.MarkdownFormatter;
import com.google.gerrit.server.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.plugins.Plugin;
import com.google.gerrit.server.plugins.ReloadPluginListener;
@@ -173,8 +174,15 @@
if (file.startsWith("Documentation/") || file.startsWith("static/")) {
JarFile jar = holder.plugin.getJarFile();
JarEntry entry = jar.getJarEntry(file);
- if (entry != null && entry.getSize() > 0) {
- sendResource(jar, entry, res);
+ if (file.startsWith("Documentation/") && !isValidEntry(entry)) {
+ entry = getRealFileEntry(jar, file);
+ if (isValidEntry(entry)) {
+ sendResource(jar, entry, res, holder.plugin.getName(), true);
+ return;
+ }
+ }
+ if (isValidEntry(entry)) {
+ sendResource(jar, entry, res, holder.plugin.getName());
return;
}
}
@@ -183,8 +191,24 @@
res.sendError(HttpServletResponse.SC_NOT_FOUND);
}
- private void sendResource(JarFile jar, JarEntry entry, HttpServletResponse res)
+ private JarEntry getRealFileEntry(JarFile jar, String file) {
+ // TODO: Replace with a loop iterating over possible formatters
+ return jar.getJarEntry(file.replaceAll("\\.html$", ".md"));
+ }
+
+ private boolean isValidEntry(JarEntry entry) {
+ return entry != null && entry.getSize() > 0;
+ }
+
+ private void sendResource(JarFile jar, JarEntry entry,
+ HttpServletResponse res, String pluginName) throws IOException {
+ sendResource(jar, entry, res, pluginName, false);
+ }
+
+ private void sendResource(JarFile jar, JarEntry entry,
+ HttpServletResponse res, String pluginName, boolean format)
throws IOException {
+ String entryName = entry.getName();
byte[] data = null;
if (entry.getSize() <= 128 * 1024) {
data = new byte[(int) entry.getSize()];
@@ -194,24 +218,44 @@
} finally {
in.close();
}
+ } else if (format == true) {
+ log.warn(String.format("Plugin '%s' file '%s' too large to format",
+ pluginName, entryName));
}
String contentType = null;
+ String charEnc = null;
Attributes atts = entry.getAttributes();
if (atts != null) {
contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
+ charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
}
+
if (contentType == null) {
- MimeType type = mimeUtil.getMimeType(entry.getName(), data);
+ MimeType type = mimeUtil.getMimeType(entryName, data);
contentType = type.toString();
}
+ if (format && data != null) {
+ if (charEnc == null) {
+ charEnc = "UTF-8";
+ }
+ MarkdownFormatter fmter = new MarkdownFormatter();
+ data = fmter.getHtmlFromMarkdown(data, charEnc);
+ res.setHeader("Content-Length", Long.toString(data.length));
+ contentType = "text/html";
+ } else {
+ res.setHeader("Content-Length", Long.toString(entry.getSize()));
+ }
+
long time = entry.getTime();
if (0 < time) {
res.setDateHeader("Last-Modified", time);
}
res.setContentType(contentType);
- res.setHeader("Content-Length", Long.toString(entry.getSize()));
+ if (charEnc != null) {
+ res.setCharacterEncoding(charEnc);
+ }
if (data != null) {
res.getOutputStream().write(data);
} else {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
index 5d7983b..5f0bc80 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
@@ -97,7 +97,6 @@
Scopes.SINGLETON);
bind(String.class).annotatedWith(CanonicalWebUrl.class)
.toProvider(CanonicalWebUrlProvider.class).in(Scopes.SINGLETON);
- bind(CachePool.class);
install(AccountCacheImpl.module());
install(GroupCacheImpl.module());
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index 93e6fb3..e592101 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -22,7 +22,7 @@
public final class AccountProjectWatch {
public enum NotifyType {
- NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES
+ NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES, ALL
}
public static final String FILTER_ALL = "*";
@@ -159,6 +159,12 @@
case SUBMITTED_CHANGES:
notifySubmittedChanges = v;
break;
+
+ case ALL:
+ notifyNewChanges = v;
+ notifyAllComments = v;
+ notifySubmittedChanges = v;
+ break;
}
}
}
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 70397d8..ceb3c55 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -170,6 +170,11 @@
<groupId>com.googlecode.prolog-cafe</groupId>
<artifactId>PrologCafe</artifactId>
</dependency>
+
+ <dependency>
+ <groupId>org.pegdown</groupId>
+ <artifactId>pegdown</artifactId>
+ </dependency>
</dependencies>
<build>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
new file mode 100644
index 0000000..2f6b422
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2012 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.
+
+package com.google.gerrit.server.documentation;
+
+import static org.pegdown.Extensions.ALL;
+
+import org.eclipse.jgit.util.RawParseUtils;
+import org.pegdown.PegDownProcessor;
+
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+
+public class MarkdownFormatter {
+
+ public byte[] getHtmlFromMarkdown(byte[] data, String charEnc)
+ throws UnsupportedEncodingException {
+ String decodedData = RawParseUtils.decode(Charset.forName(charEnc), data);
+ String formatted = new PegDownProcessor(ALL).markdownToHtml(decodedData);
+ data = formatted.getBytes(charEnc);
+ return data;
+ }
+ // TODO: Add a cache
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
new file mode 100644
index 0000000..ba2833d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2012 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.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.mail.Address;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class NotifyConfig implements Comparable<NotifyConfig> {
+ private String name;
+ private EnumSet<NotifyType> types = EnumSet.of(NotifyType.ALL);
+ private String filter;
+
+ private Set<GroupReference> groups = Sets.newHashSet();
+ private Set<Address> addresses = Sets.newHashSet();
+
+ public String getName() {
+ return name;
+ }
+
+ public void setName(String name) {
+ this.name = name;
+ }
+
+ public boolean isNotify(NotifyType type) {
+ return types.contains(type) || types.contains(NotifyType.ALL);
+ }
+
+ public EnumSet<NotifyType> getNotify() {
+ return types;
+ }
+
+ public void setTypes(EnumSet<NotifyType> newTypes) {
+ types = EnumSet.copyOf(newTypes);
+ }
+
+ public String getFilter() {
+ return filter;
+ }
+
+ public void setFilter(String filter) {
+ if ("*".equals(filter)) {
+ this.filter = null;
+ } else {
+ this.filter = Strings.emptyToNull(filter);
+ }
+ }
+
+ public Set<GroupReference> getGroups() {
+ return groups;
+ }
+
+ public Set<Address> getAddresses() {
+ return addresses;
+ }
+
+ public void addEmail(GroupReference group) {
+ groups.add(group);
+ }
+
+ public void addEmail(Address address) {
+ addresses.add(address);
+ }
+
+ @Override
+ public int compareTo(NotifyConfig o) {
+ return name.compareTo(o.name);
+ }
+
+ @Override
+ public int hashCode() {
+ return name.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj instanceof NotifyConfig) {
+ return compareTo((NotifyConfig) obj) == 0;
+ }
+ return false;
+ }
+
+ @Override
+ public String toString() {
+ return "NotifyConfig[" + name + " = " + addresses + " + " + groups + "]";
+ }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 0fe00ea..75c9bd1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -16,6 +16,8 @@
import static com.google.gerrit.common.data.Permission.isPermission;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GlobalCapability;
@@ -23,17 +25,21 @@
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.common.data.RefConfigSection;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.Project.State;
import com.google.gerrit.reviewdb.client.Project.SubmitType;
import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.mail.Address;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.StringUtils;
import java.io.BufferedReader;
import java.io.IOException;
@@ -41,6 +47,7 @@
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
+import java.util.EnumSet;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
@@ -67,6 +74,11 @@
private static final String KEY_AUTO_VERIFY = "autoVerify";
private static final String KEY_AGREEMENT_URL = "agreementUrl";
+ private static final String NOTIFY = "notify";
+ private static final String KEY_EMAIL = "email";
+ private static final String KEY_FILTER = "filter";
+ private static final String KEY_TYPE = "type";
+
private static final String CAPABILITY = "capability";
private static final String RECEIVE = "receive";
@@ -91,6 +103,7 @@
private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
private Map<String, AccessSection> accessSections;
private Map<String, ContributorAgreement> contributorAgreements;
+ private Map<String, NotifyConfig> notifySections;
private List<ValidationError> validationErrors;
private ObjectId rulesId;
@@ -185,6 +198,10 @@
contributorAgreements.put(section.getName(), section);
}
+ public Collection<NotifyConfig> getNotifyConfigs() {
+ return notifySections.values();
+ }
+
public GroupReference resolve(AccountGroup group) {
return resolve(GroupReference.forGroup(group));
}
@@ -275,6 +292,7 @@
loadAccountsSection(rc, groupsByName);
loadContributorAgreements(rc, groupsByName);
loadAccessSections(rc, groupsByName);
+ loadNotifySections(rc, groupsByName);
}
private void loadAccountsSection(
@@ -318,6 +336,67 @@
}
}
+ /**
+ * Parses the [notify] sections out of the configuration file.
+ *
+ * <pre>
+ * [notify "reviewers"]
+ * email = group Reviewers
+ * type = new_changes
+ *
+ * [notify "dev-team"]
+ * email = dev-team@example.com
+ * filter = branch:master
+ *
+ * [notify "qa"]
+ * email = qa@example.com
+ * filter = branch:\"^(maint|stable)-.*\"
+ * type = submitted_changes
+ * </pre>
+ */
+ private void loadNotifySections(
+ Config rc, Map<String, GroupReference> groupsByName) {
+ notifySections = Maps.newHashMap();
+ for (String sectionName : rc.getSubsections(NOTIFY)) {
+ NotifyConfig n = new NotifyConfig();
+ n.setName(sectionName);
+ n.setFilter(rc.getString(NOTIFY, sectionName, KEY_FILTER));
+
+ EnumSet<NotifyType> types = EnumSet.noneOf(NotifyType.class);
+ types.addAll(ConfigUtil.getEnumList(rc,
+ NOTIFY, sectionName, KEY_TYPE,
+ NotifyType.ALL));
+ n.setTypes(types);
+
+ for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
+ if (dst.startsWith("group ")) {
+ String groupName = dst.substring(6).trim();
+ GroupReference ref = groupsByName.get(groupName);
+ if (ref == null) {
+ ref = new GroupReference(null, groupName);
+ groupsByName.put(ref.getName(), ref);
+ }
+ if (ref.getUUID() != null) {
+ n.addEmail(ref);
+ } else {
+ error(new ValidationError(PROJECT_CONFIG,
+ "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+ }
+ } else if (dst.startsWith("user ")) {
+ error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
+ } else {
+ try {
+ n.addEmail(Address.parse(dst));
+ } catch (IllegalArgumentException err) {
+ error(new ValidationError(PROJECT_CONFIG,
+ "notify section \"" + sectionName + "\" has invalid email \"" + dst + "\""));
+ }
+ }
+ }
+ notifySections.put(sectionName, n);
+ }
+ }
+
private void loadAccessSections(
Config rc, Map<String, GroupReference> groupsByName) {
accessSections = new HashMap<String, AccessSection>();
@@ -458,6 +537,7 @@
saveAccountsSection(rc, keepGroups);
saveContributorAgreements(rc, keepGroups);
saveAccessSections(rc, keepGroups);
+ saveNotifySections(rc, keepGroups);
groupsByUUID.keySet().retainAll(keepGroups);
saveConfig(PROJECT_CONFIG, rc);
@@ -493,6 +573,47 @@
}
}
+ private void saveNotifySections(
+ Config rc, Set<AccountGroup.UUID> keepGroups) {
+ for (NotifyConfig nc : sort(notifySections.values())) {
+ List<String> email = Lists.newArrayList();
+ for (GroupReference gr : nc.getGroups()) {
+ if (gr.getUUID() != null) {
+ keepGroups.add(gr.getUUID());
+ }
+ email.add(new PermissionRule(gr).asString(false));
+ }
+ Collections.sort(email);
+
+ List<String> addrs = Lists.newArrayList();
+ for (Address addr : nc.getAddresses()) {
+ addrs.add(addr.toString());
+ }
+ Collections.sort(addrs);
+ email.addAll(addrs);
+
+ if (email.isEmpty()) {
+ rc.unset(NOTIFY, nc.getName(), KEY_EMAIL);
+ } else {
+ rc.setStringList(NOTIFY, nc.getName(), KEY_EMAIL, email);
+ }
+
+ if (nc.getNotify().equals(EnumSet.of(NotifyType.ALL))) {
+ rc.unset(NOTIFY, nc.getName(), KEY_TYPE);
+ } else {
+ List<String> types = Lists.newArrayListWithCapacity(4);
+ for (NotifyType t : NotifyType.values()) {
+ if (nc.isNotify(t)) {
+ types.add(StringUtils.toLowerCase(t.name()));
+ }
+ }
+ rc.setStringList(NOTIFY, nc.getName(), KEY_TYPE, types);
+ }
+
+ set(rc, NOTIFY, nc.getName(), KEY_FILTER, nc.getFilter());
+ }
+ }
+
private List<String> ruleToStringList(
List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
List<String> rules = new ArrayList<String>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
index 5bff0ad..05032b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
@@ -113,8 +113,8 @@
database = db;
replicationUserFactory = ruf;
gitRepositoryManager = grm;
- configs = allConfigs(site);
groupCache = gc;
+ configs = allConfigs(site);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index 7fabcfe1..0eb3dfe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index 624e626..4e9ed2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -55,6 +55,19 @@
}
@Override
+ public int hashCode() {
+ return email.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof Address) {
+ return email.equals(((Address) other).email);
+ }
+ return false;
+ }
+
+ @Override
public String toString() {
try {
return toHeaderString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index 100275e..e1a1725 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -14,18 +14,25 @@
package com.google.gerrit.server.mail;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.ChangeMessage;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetApproval;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.NotifyConfig;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
@@ -34,17 +41,17 @@
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeData;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SingleGroupUser;
import com.google.gwtorm.server.OrmException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.text.MessageFormat;
-import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
-import java.util.List;
+import java.util.Queue;
import java.util.Set;
import java.util.TreeSet;
@@ -305,53 +312,148 @@
// Just don't BCC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC users that starred updated change", err);
}
}
- /** BCC any user who has set "notify all comments" on this project. */
- protected void bccWatchesNotifyAllComments() {
+ /** BCC users and groups that want notification of events. */
+ protected void bccWatches(NotifyType type) {
try {
- // BCC anyone else who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.ALL_COMMENTS)) {
- add(RecipientType.BCC, w.getAccountId());
- }
+ Watchers matching = getWatches(type);
+ for (Account.Id user : matching.accounts) {
+ add(RecipientType.BCC, user);
+ }
+ for (Address addr : matching.emails) {
+ add(RecipientType.BCC, addr);
}
} catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC watchers for " + type, err);
}
}
/** Returns all watches that are relevant */
- protected final List<AccountProjectWatch> getWatches() throws OrmException {
+ protected final Watchers getWatches(NotifyType type) throws OrmException {
+ Watchers matching = new Watchers();
if (changeData == null) {
- return Collections.emptyList();
+ return matching;
}
- List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
for (AccountProjectWatch w : args.db.get().accountProjectWatches()
.byProject(change.getProject())) {
projectWatchers.add(w.getAccountId());
- add(matching, w);
- }
-
- for (AccountProjectWatch w : args.db.get().accountProjectWatches()
- .byProject(args.allProjectsName)) {
- if (!projectWatchers.contains(w.getAccountId())) {
+ if (w.isNotify(type)) {
add(matching, w);
}
}
- return Collections.unmodifiableList(matching);
+ for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+ .byProject(args.allProjectsName)) {
+ if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
+ add(matching, w);
+ }
+ }
+
+ ProjectState state = projectState;
+ while (state != null) {
+ for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
+ if (nc.isNotify(type)) {
+ try {
+ add(matching, nc, state.getProject().getNameKey());
+ } catch (QueryParseException e) {
+ log.warn(String.format(
+ "Project %s has invalid notify %s filter \"%s\": %s",
+ state.getProject().getName(), nc.getName(),
+ nc.getFilter(), e.getMessage()));
+ }
+ }
+ }
+ state = state.getParentState();
+ }
+
+ return matching;
+ }
+
+ protected static class Watchers {
+ protected final Set<Account.Id> accounts = Sets.newHashSet();
+ protected final Set<Address> emails = Sets.newHashSet();
}
@SuppressWarnings("unchecked")
- private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
+ private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
+ throws OrmException, QueryParseException {
+ for (GroupReference ref : nc.getGroups()) {
+ AccountGroup group = args.groupCache.get(ref.getUUID());
+ if (group == null) {
+ log.warn(String.format(
+ "Project %s has invalid group %s in notify section %s",
+ project.get(), ref.getName(), nc.getName()));
+ continue;
+ }
+
+ if (group.getType() != AccountGroup.Type.INTERNAL) {
+ log.warn(String.format(
+ "Project %s cannot use group %s of type %s in notify section %s",
+ project.get(), ref.getName(), group.getType(), nc.getName()));
+ continue;
+ }
+
+ ChangeQueryBuilder qb = args.queryBuilder.create(new SingleGroupUser(
+ args.capabilityControlFactory,
+ ref.getUUID()));
+ qb.setAllowFile(true);
+ Predicate<ChangeData> p = qb.is_visible();
+ if (nc.getFilter() != null) {
+ p = Predicate.and(qb.parse(nc.getFilter()), p);
+ p = args.queryRewriter.get().rewrite(p);
+ }
+ if (p.match(changeData)) {
+ recursivelyAddAllAccounts(matching, group);
+ }
+ }
+
+ if (!nc.getAddresses().isEmpty()) {
+ if (nc.getFilter() != null) {
+ ChangeQueryBuilder qb = args.queryBuilder.create(args.anonymousUser);
+ qb.setAllowFile(true);
+ Predicate<ChangeData> p = qb.parse(nc.getFilter());
+ p = args.queryRewriter.get().rewrite(p);
+ if (p.match(changeData)) {
+ matching.emails.addAll(nc.getAddresses());
+ }
+ } else {
+ matching.emails.addAll(nc.getAddresses());
+ }
+ }
+ }
+
+ private void recursivelyAddAllAccounts(Watchers matching, AccountGroup group)
+ throws OrmException {
+ Set<AccountGroup.Id> seen = Sets.newHashSet();
+ Queue<AccountGroup.Id> scan = Lists.newLinkedList();
+ scan.add(group.getId());
+ seen.add(group.getId());
+ while (!scan.isEmpty()) {
+ AccountGroup.Id next = scan.remove();
+ for (AccountGroupMember m : args.db.get().accountGroupMembers()
+ .byGroup(next)) {
+ matching.accounts.add(m.getAccountId());
+ }
+ for (AccountGroupInclude m : args.db.get().accountGroupIncludes()
+ .byGroup(next)) {
+ if (seen.add(m.getIncludeId())) {
+ scan.add(m.getIncludeId());
+ }
+ }
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ private void add(Watchers matching, AccountProjectWatch w)
throws OrmException {
IdentifiedUser user =
args.identifiedUserFactory.create(args.db, w.getAccountId());
@@ -363,13 +465,13 @@
p = Predicate.and(qb.parse(w.getFilter()), p);
p = args.queryRewriter.get().rewrite(p);
if (p.match(changeData)) {
- matching.add(w);
+ matching.accounts.add(w.getAccountId());
}
} catch (QueryParseException e) {
// Ignore broken filter expressions.
}
} else if (p.match(changeData)) {
- matching.add(w);
+ matching.accounts.add(w.getAccountId());
}
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index f054ee8..749b11f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -17,6 +17,7 @@
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Patch;
import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.patch.PatchFile;
import com.google.gerrit.server.patch.PatchList;
@@ -68,7 +69,7 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index c6f716d..9b82c71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -15,74 +15,65 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
-import java.util.HashSet;
-import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
/** Notify interested parties of a brand new change. */
public class CreateChangeSender extends NewChangeSender {
+ private static final Logger log =
+ LoggerFactory.getLogger(CreateChangeSender.class);
+
public static interface Factory {
public CreateChangeSender create(Change change);
}
- private final GroupCache groupCache;
-
@Inject
public CreateChangeSender(EmailArguments ea,
@AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
- GroupCache groupCache, @Assisted Change c) {
+ @Assisted Change c) {
super(ea, anonymousCowardName, sshInfo, c);
- this.groupCache = groupCache;
}
@Override
protected void init() throws EmailException {
super.init();
- bccWatchers();
- }
-
- private void bccWatchers() {
try {
+ // BCC anyone who has interest in this project's changes
// Try to mark interested owners with a TO and not a BCC line.
//
- final Set<Account.Id> owners = new HashSet<Account.Id>();
- for (AccountGroup.UUID uuid : getProjectOwners()) {
- AccountGroup group = groupCache.get(uuid);
- if (group != null) {
- for (AccountGroupMember m : args.db.get().accountGroupMembers()
- .byGroup(group.getId())) {
- owners.add(m.getAccountId());
- }
+ Watchers matching = getWatches(NotifyType.NEW_CHANGES);
+ for (Account.Id user : matching.accounts) {
+ if (isOwnerOfProjectOrBranch(user)) {
+ add(RecipientType.TO, user);
+ } else {
+ add(RecipientType.BCC, user);
}
}
-
- // BCC anyone who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.NEW_CHANGES)) {
- if (owners.contains(w.getAccountId())) {
- add(RecipientType.TO, w.getAccountId());
- } else {
- add(RecipientType.BCC, w.getAccountId());
- }
- }
+ for (Address addr : matching.emails) {
+ add(RecipientType.BCC, addr);
}
} catch (OrmException err) {
// Just don't CC everyone. Better to send a partial message to those
// we already have queued up then to fail deliver entirely to people
// who have a lower interest in the change.
+ log.warn("Cannot BCC watchers for new change", err);
}
}
+
+ private boolean isOwnerOfProjectOrBranch(Account.Id user) {
+ return projectState != null
+ && change != null
+ && projectState.controlFor(args.identifiedUserFactory.create(user))
+ .controlForRef(change.getDest())
+ .isOwner();
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 68f78d0..fa49b06 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -15,9 +15,11 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.IdentifiedUser.GenericFactory;
import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -44,6 +46,8 @@
final EmailSender emailSender;
final PatchSetInfoFactory patchSetInfoFactory;
final IdentifiedUser.GenericFactory identifiedUserFactory;
+ final CapabilityControl.Factory capabilityControlFactory;
+ final AnonymousUser anonymousUser;
final Provider<String> urlProvider;
final AllProjectsName allProjectsName;
@@ -58,6 +62,8 @@
PatchListCache patchListCache, FromAddressGenerator fromAddressGenerator,
EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory,
GenericFactory identifiedUserFactory,
+ CapabilityControl.Factory capabilityControlFactory,
+ AnonymousUser anonymousUser,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
AllProjectsName allProjectsName,
ChangeQueryBuilder.Factory queryBuilder,
@@ -72,6 +78,8 @@
this.emailSender = emailSender;
this.patchSetInfoFactory = patchSetInfoFactory;
this.identifiedUserFactory = identifiedUserFactory;
+ this.capabilityControlFactory = capabilityControlFactory;
+ this.anonymousUser = anonymousUser;
this.urlProvider = urlProvider;
this.allProjectsName = allProjectsName;
this.queryBuilder = queryBuilder;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 3590b8a..70b2d7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -17,7 +17,6 @@
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.common.data.ApprovalTypes;
import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
import com.google.gerrit.reviewdb.client.ApprovalCategory;
import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.client.Change;
@@ -53,8 +52,8 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
- bccWatchesNotifySubmittedChanges();
+ bccWatches(NotifyType.ALL_COMMENTS);
+ bccWatches(NotifyType.SUBMITTED_CHANGES);
}
@Override
@@ -140,20 +139,4 @@
}
m.put(ca.getCategoryId(), ca);
}
-
- private void bccWatchesNotifySubmittedChanges() {
- try {
- // BCC anyone else who has interest in this project's changes
- //
- for (final AccountProjectWatch w : getWatches()) {
- if (w.isNotify(NotifyType.SUBMITTED_CHANGES)) {
- add(RecipientType.BCC, w.getAccountId());
- }
- }
- } catch (OrmException err) {
- // Just don't CC everyone. Better to send a partial message to those
- // we already have queued up then to fail deliver entirely to people
- // who have a lower interest in the change.
- }
- }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index de8628f..caa441d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -14,6 +14,7 @@
package com.google.gerrit.server.mail;
+import com.google.common.collect.Sets;
import com.google.gerrit.reviewdb.client.Account;
import com.google.gerrit.reviewdb.client.UserIdentity;
import com.google.gerrit.server.account.AccountState;
@@ -34,14 +35,13 @@
import java.io.StringWriter;
import java.net.MalformedURLException;
import java.net.URL;
-import java.util.ArrayList;
import java.util.Collection;
import java.util.Date;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedHashMap;
-import java.util.List;
import java.util.Map;
+import java.util.Set;
/** Sends an email to one or more interested parties. */
public abstract class OutgoingEmail {
@@ -53,7 +53,7 @@
protected String messageClass;
private final HashSet<Account.Id> rcptTo = new HashSet<Account.Id>();
private final Map<String, EmailHeader> headers;
- private final List<Address> smtpRcptTo = new ArrayList<Address>();
+ private final Set<Address> smtpRcptTo = Sets.newHashSet();
private Address smtpFromAddress;
private StringBuilder body;
protected VelocityContext velocityContext;
@@ -282,7 +282,7 @@
return false;
}
- if (rcptTo.size() == 1 && rcptTo.contains(fromId)) {
+ if (smtpRcptTo.size() == 1 && rcptTo.size() == 1 && rcptTo.contains(fromId)) {
// If the only recipient is also the sender, don't bother.
//
return false;
@@ -324,14 +324,15 @@
protected void add(final RecipientType rt, final Address addr) {
if (addr != null && addr.email != null && addr.email.length() > 0) {
if (args.emailSender.canEmail(addr.email)) {
- smtpRcptTo.add(addr);
- switch (rt) {
- case TO:
- ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
- break;
- case CC:
- ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
- break;
+ if (smtpRcptTo.add(addr)) {
+ switch (rt) {
+ case TO:
+ ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+ break;
+ case CC:
+ ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+ break;
+ }
}
} else {
log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index c9afdde..946c29f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index 964bfed..033bd56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
import com.google.gerrit.server.config.AnonymousCowardName;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -37,7 +38,7 @@
ccAllApprovals();
bccStarredBy();
- bccWatchesNotifyAllComments();
+ bccWatches(NotifyType.ALL_COMMENTS);
}
@Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index 77e082d..b0f12b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -43,6 +43,12 @@
* @type <T> type of object the predicate can evaluate in memory.
*/
public abstract class Predicate<T> {
+ /** A predicate that matches any input, always, with no cost. */
+ @SuppressWarnings("unchecked")
+ public static <T> Predicate<T> any() {
+ return (Predicate<T>) Any.INSTANCE;
+ }
+
/** Combine the passed predicates into a single AND node. */
public static <T> Predicate<T> and(final Predicate<T>... that) {
if (that.length == 1) {
@@ -120,4 +126,36 @@
@Override
public abstract boolean equals(Object other);
+
+ private static class Any<T> extends Predicate<T> {
+ private static final Any<Object> INSTANCE = new Any<Object>();
+
+ private Any() {
+ }
+
+ @Override
+ public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
+ return this;
+ }
+
+ @Override
+ public boolean match(T object) {
+ return true;
+ }
+
+ @Override
+ public int getCost() {
+ return 0;
+ }
+
+ @Override
+ public int hashCode() {
+ return System.identityHashCode(this);
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return other == this;
+ }
+ }
}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index cdce217..270b2e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -27,15 +27,15 @@
import java.util.Collections;
import java.util.Set;
-final class SingleGroupUser extends CurrentUser {
+public final class SingleGroupUser extends CurrentUser {
private final GroupMembership groups;
- SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+ public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
AccountGroup.UUID groupId) {
this(capabilityControlFactory, Collections.singleton(groupId));
}
- SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+ public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
Set<AccountGroup.UUID> groups) {
super(capabilityControlFactory, AccessPath.UNKNOWN);
this.groups = new ListGroupMembership(groups);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 08bfaaa..66e6add 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -14,6 +14,8 @@
package com.google.gerrit.sshd;
+import com.google.common.util.concurrent.Atomics;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.git.WorkQueue;
import com.google.gerrit.sshd.SshScope.Context;
@@ -36,7 +38,11 @@
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
/**
* Creates a CommandFactory using commands registered by {@link CommandModule}.
@@ -47,7 +53,8 @@
private final DispatchCommandProvider dispatcher;
private final SshLog log;
- private final Executor startExecutor;
+ private final ScheduledExecutorService startExecutor;
+ private final Executor destroyExecutor;
@Inject
CommandFactoryProvider(
@@ -59,6 +66,11 @@
int threads = cfg.getInt("sshd","commandStartThreads", 2);
startExecutor = workQueue.createQueue(threads, "SshCommandStart");
+ destroyExecutor = Executors.newSingleThreadExecutor(
+ new ThreadFactoryBuilder()
+ .setNameFormat("SshCommandDestroy-%s")
+ .setDaemon(true)
+ .build());
}
@Override
@@ -81,11 +93,13 @@
private Context ctx;
private DispatchCommand cmd;
private final AtomicBoolean logged;
+ private final AtomicReference<Future<?>> task;
Trampoline(final String cmdLine) {
commandLine = cmdLine;
argv = split(cmdLine);
logged = new AtomicBoolean();
+ task = Atomics.newReference();
}
public void setInputStream(final InputStream in) {
@@ -112,7 +126,7 @@
public void start(final Environment env) throws IOException {
this.env = env;
final Context ctx = this.ctx;
- startExecutor.execute(new Runnable() {
+ task.set(startExecutor.submit(new Runnable() {
public void run() {
try {
onStart();
@@ -126,7 +140,7 @@
public String toString() {
return "start (user " + ctx.getSession().getUsername() + ")";
}
- });
+ }));
}
private void onStart() throws IOException {
@@ -182,6 +196,19 @@
@Override
public void destroy() {
+ Future<?> future = task.getAndSet(null);
+ if (future != null) {
+ future.cancel(true);
+ destroyExecutor.execute(new Runnable() {
+ @Override
+ public void run() {
+ onDestroy();
+ }
+ });
+ }
+ }
+
+ private void onDestroy() {
synchronized (this) {
if (cmd != null) {
final Context old = SshScope.set(ctx);
diff --git a/pom.xml b/pom.xml
index f366c4d..78dcebb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -822,6 +822,12 @@
<artifactId>PrologCafe</artifactId>
<version>1.3</version>
</dependency>
+
+ <dependency>
+ <groupId>org.pegdown</groupId>
+ <artifactId>pegdown</artifactId>
+ <version>1.1.0</version>
+ </dependency>
</dependencies>
</dependencyManagement>
@@ -850,5 +856,10 @@
<id>clojars-repo</id>
<url>http://clojars.org/repo</url>
</repository>
+
+ <repository>
+ <id>scala-tools</id>
+ <url>http://scala-tools.org/repo-releases</url>
+ </repository>
</repositories>
</project>