Allow watching specific branches or any other search query

Any valid search string is now a valid filter expression or a
watched project.  The only operator not supported here is the
is:watched operator, because that creates a recursive call that
would never succeed.

The change turned out far bigger than it should be due to the request
scope requirement for the query builder.  We had to rearrange a lot
of code to ensure we always have the request scope available in order
to construct a query builder and execute the filter expressions.

Bug: issue 492
Change-Id: I199d9b215e000c049279cd8e86e7a36386fee0fb
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Shawn O. Pearce 2010-07-19 16:15:08 -07:00
parent 14760b7c0e
commit 0f42fc05a4
46 changed files with 908 additions and 434 deletions

View File

@ -48,7 +48,7 @@ public interface AccountService extends RemoteJsonService {
void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
@SignInRequired
void addProjectWatch(String projectName,
void addProjectWatch(String projectName, String filter,
AsyncCallback<AccountProjectWatchInfo> callback);
@SignInRequired

View File

@ -32,6 +32,7 @@ public interface GerritCss extends CssResource {
String removeReviewer();
String removeReviewerCell();
String addSshKeyPanel();
String addWatchPanel();
String approvalCategoryList();
String approvalTable();
String approvalhint();
@ -180,4 +181,5 @@ public interface GerritCss extends CssResource {
String useridentity();
String usernameField();
String version();
String watchedProjectFilter();
}

View File

@ -80,6 +80,9 @@ public interface AccountConstants extends Constants {
String buttonWatchProject();
String defaultProjectName();
String defaultFilter();
String watchedProjectName();
String watchedProjectFilter();
String watchedProjectColumnEmailNotifications();
String watchedProjectColumnNewChanges();
String watchedProjectColumnAllComments();

View File

@ -61,6 +61,9 @@ sshJavaAppletNotAvailable = Open Key Unavailable: Java not enabled
buttonWatchProject = Watch
defaultProjectName = Project Name
defaultFilter = branch:name, or other search expression
watchedProjectName = Project Name
watchedProjectFilter = Only If
watchedProjectColumnEmailNotifications = Email Notifications
watchedProjectColumnNewChanges = New Changes
watchedProjectColumnAllComments = All Comments

View File

@ -38,8 +38,11 @@ import com.google.gwt.user.client.DOM;
import com.google.gwt.user.client.ui.Button;
import com.google.gwt.user.client.ui.CheckBox;
import com.google.gwt.user.client.ui.FlowPanel;
import com.google.gwt.user.client.ui.Grid;
import com.google.gwt.user.client.ui.Label;
import com.google.gwt.user.client.ui.SuggestBox;
import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
import com.google.gwtexpui.globalkey.client.NpTextBox;
import com.google.gwtjsonrpc.client.VoidResult;
@ -51,7 +54,9 @@ public class MyWatchedProjectsScreen extends SettingsScreen {
private WatchTable watches;
private Button addNew;
private NpTextBox nameBox;
private SuggestBox nameTxt;
private NpTextBox filterTxt;
private Button delSel;
private boolean submitOnSelection;
@ -60,32 +65,30 @@ public class MyWatchedProjectsScreen extends SettingsScreen {
super.onInitUI();
{
final FlowPanel fp = new FlowPanel();
final NpTextBox box = new NpTextBox();
nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), box);
box.setVisibleLength(50);
box.setText(Util.C.defaultProjectName());
box.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
box.addFocusHandler(new FocusHandler() {
nameBox = new NpTextBox();
nameTxt = new SuggestBox(new ProjectNameSuggestOracle(), nameBox);
nameBox.setVisibleLength(50);
nameBox.setText(Util.C.defaultProjectName());
nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
nameBox.addFocusHandler(new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
if (Util.C.defaultProjectName().equals(box.getText())) {
box.setText("");
box.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
if (Util.C.defaultProjectName().equals(nameBox.getText())) {
nameBox.setText("");
nameBox.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
}
}
});
box.addBlurHandler(new BlurHandler() {
nameBox.addBlurHandler(new BlurHandler() {
@Override
public void onBlur(BlurEvent event) {
if ("".equals(box.getText())) {
box.setText(Util.C.defaultProjectName());
box.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
if ("".equals(nameBox.getText())) {
nameBox.setText(Util.C.defaultProjectName());
nameBox.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
}
}
});
box.addKeyPressHandler(new KeyPressHandler() {
nameBox.addKeyPressHandler(new KeyPressHandler() {
@Override
public void onKeyPress(KeyPressEvent event) {
submitOnSelection = false;
@ -108,7 +111,37 @@ public class MyWatchedProjectsScreen extends SettingsScreen {
}
}
});
fp.add(nameTxt);
filterTxt = new NpTextBox();
filterTxt.setVisibleLength(50);
filterTxt.setText(Util.C.defaultFilter());
filterTxt.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
filterTxt.addFocusHandler(new FocusHandler() {
@Override
public void onFocus(FocusEvent event) {
if (Util.C.defaultFilter().equals(filterTxt.getText())) {
filterTxt.setText("");
filterTxt.removeStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
}
}
});
filterTxt.addBlurHandler(new BlurHandler() {
@Override
public void onBlur(BlurEvent event) {
if ("".equals(filterTxt.getText())) {
filterTxt.setText(Util.C.defaultFilter());
filterTxt.addStyleName(Gerrit.RESOURCES.css().inputFieldTypeHint());
}
}
});
filterTxt.addKeyPressHandler(new KeyPressHandler() {
@Override
public void onKeyPress(KeyPressEvent event) {
if (event.getCharCode() == KeyCodes.KEY_ENTER) {
doAddNew();
}
}
});
addNew = new Button(Util.C.buttonWatchProject());
addNew.addClickHandler(new ClickHandler() {
@ -117,24 +150,40 @@ public class MyWatchedProjectsScreen extends SettingsScreen {
doAddNew();
}
});
final Grid grid = new Grid(2, 2);
grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
grid.setText(0, 0, Util.C.watchedProjectName());
grid.setWidget(0, 1, nameTxt);
grid.setText(1, 0, Util.C.watchedProjectFilter());
grid.setWidget(1, 1, filterTxt);
final CellFormatter fmt = grid.getCellFormatter();
fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().header());
fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().header());
fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().bottomheader());
final FlowPanel fp = new FlowPanel();
fp.setStyleName(Gerrit.RESOURCES.css().addWatchPanel());
fp.add(grid);
fp.add(addNew);
add(fp);
}
watches = new WatchTable();
add(watches);
{
final FlowPanel fp = new FlowPanel();
delSel = new Button(Util.C.buttonDeleteSshKey());
delSel.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
watches.deleteChecked();
}
});
fp.add(delSel);
add(fp);
}
delSel = new Button(Util.C.buttonDeleteSshKey());
delSel.addClickHandler(new ClickHandler() {
@Override
public void onClick(final ClickEvent event) {
watches.deleteChecked();
}
});
add(delSel);
}
void doAddNew() {
@ -144,11 +193,23 @@ public class MyWatchedProjectsScreen extends SettingsScreen {
return;
}
String filter = filterTxt.getText();
if (filter == null || filter.isEmpty()
|| filter.equals(Util.C.defaultFilter())) {
filter = null;
}
addNew.setEnabled(false);
Util.ACCOUNT_SVC.addProjectWatch(projectName,
nameBox.setEnabled(false);
filterTxt.setEnabled(false);
Util.ACCOUNT_SVC.addProjectWatch(projectName, filter,
new GerritCallback<AccountProjectWatchInfo>() {
public void onSuccess(final AccountProjectWatchInfo result) {
addNew.setEnabled(true);
nameBox.setEnabled(true);
filterTxt.setEnabled(true);
nameTxt.setText("");
watches.insertWatch(result);
}
@ -156,6 +217,9 @@ public class MyWatchedProjectsScreen extends SettingsScreen {
@Override
public void onFailure(final Throwable caught) {
addNew.setEnabled(true);
nameBox.setEnabled(true);
filterTxt.setEnabled(true);
super.onFailure(caught);
}
});
@ -177,8 +241,7 @@ public class MyWatchedProjectsScreen extends SettingsScreen {
WatchTable() {
table.setWidth("");
table.insertRow(1);
table.setText(0, 2, com.google.gerrit.client.changes.Util.C
.changeTableColumnProject());
table.setText(0, 2, Util.C.watchedProjectName());
table.setText(0, 3, Util.C.watchedProjectColumnEmailNotifications());
final FlexCellFormatter fmt = table.getFlexCellFormatter();
@ -253,8 +316,16 @@ public class MyWatchedProjectsScreen extends SettingsScreen {
}
void populate(final int row, final AccountProjectWatchInfo k) {
final FlowPanel fp = new FlowPanel();
fp.add(new ProjectLink(k.getProject().getNameKey(), Status.NEW));
if (k.getWatch().getFilter() != null) {
Label filter = new Label(k.getWatch().getFilter());
filter.setStyleName(Gerrit.RESOURCES.css().watchedProjectFilter());
fp.add(filter);
}
table.setWidget(row, 1, new CheckBox());
table.setWidget(row, 2, new ProjectLink(k.getProject().getNameKey(), Status.NEW));
table.setWidget(row, 2, fp);
{
final CheckBox notifyNewChanges = new CheckBox();
notifyNewChanges.addClickHandler(new ClickHandler() {

View File

@ -1029,6 +1029,15 @@ a:hover.downloadLink {
font-weight: bold;
}
.addWatchPanel {
margin-top: 10px;
padding: 5px 5px 5px 5px;
}
.watchedProjectFilter {
margin-left: 1em;
color: grey;
}
.addSshKeyPanel {
margin-top: 10px;
background-color: trimColor;

View File

@ -89,7 +89,7 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements
private final ChangeControl.Factory changeControlFactory;
private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
private final Provider<ChangeQueryBuilder> queryBuilder;
private final ChangeQueryBuilder.Factory queryBuilder;
private final Provider<ChangeQueryRewriter> queryRewriter;
@Inject
@ -97,7 +97,7 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements
final Provider<CurrentUser> currentUser,
final ChangeControl.Factory changeControlFactory,
final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
final Provider<ChangeQueryBuilder> queryBuilder,
final ChangeQueryBuilder.Factory queryBuilder,
final Provider<ChangeQueryRewriter> queryRewriter) {
super(schema, currentUser);
this.currentUser = currentUser;
@ -144,10 +144,8 @@ public class ChangeListServiceImpl extends BaseServiceImplementation implements
final int limit, final String key, final Comparator<Change> cmp)
throws OrmException, InvalidQueryException {
try {
final ChangeQueryBuilder builder = queryBuilder.get();
final Predicate<ChangeData> visibleToMe =
builder.visibleto(currentUser.get());
final ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
final Predicate<ChangeData> visibleToMe = builder.is_visible();
Predicate<ChangeData> q = builder.parse(query);
q = Predicate.and(q, //
cmp == QUERY_PREV //

View File

@ -17,6 +17,7 @@ package com.google.gerrit.httpd.rpc.account;
import com.google.gerrit.common.data.AccountProjectWatchInfo;
import com.google.gerrit.common.data.AccountService;
import com.google.gerrit.common.data.AgreementInfo;
import com.google.gerrit.common.errors.InvalidQueryException;
import com.google.gerrit.common.errors.NoSuchEntityException;
import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
import com.google.gerrit.reviewdb.Account;
@ -29,8 +30,11 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gwt.user.client.rpc.AsyncCallback;
import com.google.gwtjsonrpc.client.VoidResult;
import com.google.gwtorm.client.OrmDuplicateKeyException;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
@ -47,18 +51,21 @@ class AccountServiceImpl extends BaseServiceImplementation implements
private final AccountCache accountCache;
private final ProjectControl.Factory projectControlFactory;
private final AgreementInfoFactory.Factory agreementInfoFactory;
private final ChangeQueryBuilder.Factory queryBuilder;
@Inject
AccountServiceImpl(final Provider<ReviewDb> schema,
final Provider<IdentifiedUser> identifiedUser,
final AccountCache accountCache,
final ProjectControl.Factory projectControlFactory,
final AgreementInfoFactory.Factory agreementInfoFactory) {
final AgreementInfoFactory.Factory agreementInfoFactory,
final ChangeQueryBuilder.Factory queryBuilder) {
super(schema, identifiedUser);
this.currentUser = identifiedUser;
this.accountCache = accountCache;
this.projectControlFactory = projectControlFactory;
this.agreementInfoFactory = agreementInfoFactory;
this.queryBuilder = queryBuilder;
}
public void myAccount(final AsyncCallback<Account> callback) {
@ -137,19 +144,31 @@ class AccountServiceImpl extends BaseServiceImplementation implements
});
}
public void addProjectWatch(final String projectName,
public void addProjectWatch(final String projectName, final String filter,
final AsyncCallback<AccountProjectWatchInfo> callback) {
run(callback, new Action<AccountProjectWatchInfo>() {
public AccountProjectWatchInfo run(ReviewDb db) throws OrmException,
NoSuchProjectException {
NoSuchProjectException, InvalidQueryException {
final Project.NameKey nameKey = new Project.NameKey(projectName);
final ProjectControl ctl = projectControlFactory.validateFor(nameKey);
final AccountProjectWatch watch =
new AccountProjectWatch(
new AccountProjectWatch.Key(((IdentifiedUser) ctl
.getCurrentUser()).getAccountId(), nameKey));
db.accountProjectWatches().insert(Collections.singleton(watch));
if (filter != null) {
try {
queryBuilder.create(currentUser.get()).parse(filter);
} catch (QueryParseException badFilter) {
throw new InvalidQueryException(badFilter.getMessage(), filter);
}
}
AccountProjectWatch watch =
new AccountProjectWatch(new AccountProjectWatch.Key(
((IdentifiedUser) ctl.getCurrentUser()).getAccountId(),
nameKey, filter));
try {
db.accountProjectWatches().insert(Collections.singleton(watch));
} catch (OrmDuplicateKeyException alreadyHave) {
watch = db.accountProjectWatches().get(watch.getKey());
}
return new AccountProjectWatchInfo(watch, ctl.getProject());
}
});

View File

@ -127,7 +127,6 @@ class AbandonChange extends Handler<ChangeDetail> {
// Email the reviewers
final AbandonedSender cm = abandonedSenderFactory.create(change);
cm.setFrom(currentUser.getAccountId());
cm.setReviewDb(db);
cm.setChangeMessage(cmsg);
cm.send();
}

View File

@ -23,6 +23,7 @@ import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.CanSubmitResult;
@ -44,8 +45,8 @@ class SubmitAction extends Handler<ChangeDetail> {
private final FunctionState.Factory functionState;
private final IdentifiedUser user;
private final ChangeDetailFactory.Factory changeDetailFactory;
@Inject
private ChangeControl.Factory changeControlFactory;
private final ChangeControl.Factory changeControlFactory;
private final MergeOp.Factory opFactory;
private final PatchSet.Id patchSetId;
@ -53,13 +54,17 @@ class SubmitAction extends Handler<ChangeDetail> {
SubmitAction(final ReviewDb db, final MergeQueue mq, final ApprovalTypes at,
final FunctionState.Factory fs, final IdentifiedUser user,
final ChangeDetailFactory.Factory changeDetailFactory,
final ChangeControl.Factory changeControlFactory,
final MergeOp.Factory opFactory,
@Assisted final PatchSet.Id patchSetId) {
this.db = db;
this.merger = mq;
this.approvalTypes = at;
this.functionState = fs;
this.user = user;
this.changeControlFactory = changeControlFactory;
this.changeDetailFactory = changeDetailFactory;
this.opFactory = opFactory;
this.patchSetId = patchSetId;
}
@ -76,7 +81,7 @@ class SubmitAction extends Handler<ChangeDetail> {
CanSubmitResult err =
changeControl.canSubmit(patchSetId, db, approvalTypes, functionState);
if (err == CanSubmitResult.OK) {
ChangeUtil.submit(patchSetId, user, db, merger);
ChangeUtil.submit(opFactory, patchSetId, user, db, merger);
return changeDetailFactory.create(changeId).call();
} else {
throw new IllegalStateException(err.getMessage());

View File

@ -130,7 +130,6 @@ class AddReviewer extends Handler<ReviewerResult> {
final AddReviewerSender cm;
cm = addReviewerSenderFactory.create(control.getChange());
cm.setFrom(currentUser.getAccountId());
cm.setReviewDb(db);
cm.addReviewers(added);
cm.send();

View File

@ -16,9 +16,12 @@ package com.google.gerrit.reviewdb;
import com.google.gwtorm.client.Column;
import com.google.gwtorm.client.CompoundKey;
import com.google.gwtorm.client.StringKey;
/** An {@link Account} interested in a {@link Project}. */
public final class AccountProjectWatch {
public static final String FILTER_ALL = "*";
public static class Key extends CompoundKey<Account.Id> {
private static final long serialVersionUID = 1L;
@ -28,14 +31,19 @@ public final class AccountProjectWatch {
@Column(id = 2)
protected Project.NameKey projectName;
@Column(id = 3)
protected Filter filter;
protected Key() {
accountId = new Account.Id();
projectName = new Project.NameKey();
filter = new Filter();
}
public Key(final Account.Id a, final Project.NameKey g) {
public Key(Account.Id a, Project.NameKey g, String f) {
accountId = a;
projectName = g;
filter = new Filter(f);
}
@Override
@ -45,7 +53,31 @@ public final class AccountProjectWatch {
@Override
public com.google.gwtorm.client.Key<?>[] members() {
return new com.google.gwtorm.client.Key<?>[] {projectName};
return new com.google.gwtorm.client.Key<?>[] {projectName, filter};
}
}
public static class Filter extends StringKey<com.google.gwtorm.client.Key<?>> {
private static final long serialVersionUID = 1L;
@Column(id = 1)
protected String filter;
protected Filter() {
}
public Filter(String f) {
filter = f != null && !f.isEmpty() ? f : FILTER_ALL;
}
@Override
public String get() {
return filter;
}
@Override
protected void set(String newValue) {
filter = newValue;
}
}
@ -83,6 +115,10 @@ public final class AccountProjectWatch {
return key.projectName;
}
public String getFilter() {
return FILTER_ALL.equals(key.filter.get()) ? null : key.filter.get();
}
public boolean isNotifyNewChanges() {
return notifyNewChanges;
}

View File

@ -15,12 +15,13 @@
package com.google.gerrit.server;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project.NameKey;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
@ -43,7 +44,7 @@ public class AnonymousUser extends CurrentUser {
}
@Override
public Set<NameKey> getWatchedProjects() {
public Collection<AccountProjectWatch> getNotificationFilters() {
return Collections.emptySet();
}

View File

@ -23,6 +23,7 @@ import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.TrackingId;
import com.google.gerrit.server.config.TrackingFooter;
import com.google.gerrit.server.config.TrackingFooters;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gwtorm.client.AtomicUpdate;
import com.google.gwtorm.client.OrmConcurrencyException;
@ -135,8 +136,8 @@ public class ChangeUtil {
db.trackingIds().delete(toDelete);
}
public static void submit(PatchSet.Id patchSetId, IdentifiedUser user, ReviewDb db, MergeQueue merger)
throws OrmException {
public static void submit(MergeOp.Factory opFactory, PatchSet.Id patchSetId,
IdentifiedUser user, ReviewDb db, MergeQueue merger) throws OrmException {
final Change.Id changeId = patchSetId.getParentKey();
final PatchSetApproval approval = createSubmitApproval(patchSetId, user, db);
@ -154,7 +155,7 @@ public class ChangeUtil {
});
if (change.getStatus() == Change.Status.SUBMITTED) {
merger.merge(change.getDest());
merger.merge(opFactory, change.getDest());
}
}

View File

@ -15,11 +15,12 @@
package com.google.gerrit.server;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.servlet.RequestScoped;
import java.util.Collection;
import java.util.Set;
/**
@ -60,8 +61,8 @@ public abstract class CurrentUser {
/** Set of changes starred by this user. */
public abstract Set<Change.Id> getStarredChanges();
/** Set of project that are watched by this user */
public abstract Set<Project.NameKey> getWatchedProjects();
/** Filters selecting changes the user wants to monitor. */
public abstract Collection<AccountProjectWatch> getNotificationFilters();
/** Is the user a non-interactive user? */
public boolean isBatchUser() {

View File

@ -19,7 +19,6 @@ import com.google.gerrit.reviewdb.AccountDiffPreference;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.StarredChange;
import com.google.gerrit.server.account.AccountCache;
@ -43,9 +42,11 @@ import java.net.InetSocketAddress;
import java.net.MalformedURLException;
import java.net.SocketAddress;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.TimeZone;
@ -145,7 +146,7 @@ public class IdentifiedUser extends CurrentUser {
private Set<String> emailAddresses;
private Set<AccountGroup.Id> effectiveGroups;
private Set<Change.Id> starredChanges;
private Set<Project.NameKey> watchedProjects;
private Collection<AccountProjectWatch> notificationFilters;
private IdentifiedUser(final AccessPath accessPath,
final AuthConfig authConfig, final Provider<String> canonicalUrl,
@ -237,24 +238,22 @@ public class IdentifiedUser extends CurrentUser {
}
@Override
public Set<Project.NameKey> getWatchedProjects() {
if (watchedProjects == null) {
public Collection<AccountProjectWatch> getNotificationFilters() {
if (notificationFilters == null) {
if (dbProvider == null) {
throw new OutOfScopeException("Not in request scoped user");
}
final Set<Project.NameKey> h = new HashSet<Project.NameKey>();
List<AccountProjectWatch> r;
try {
for (AccountProjectWatch projectWatch : dbProvider.get()
.accountProjectWatches().byAccount(getAccountId())) {
h.add(projectWatch.getProjectNameKey());
}
r = dbProvider.get().accountProjectWatches() //
.byAccount(getAccountId()).toList();
} catch (OrmException e) {
log.warn("Cannot query project watches of a user", e);
log.warn("Cannot query notification filters of a user", e);
r = Collections.emptyList();
}
watchedProjects = Collections.unmodifiableSet(h);
notificationFilters = Collections.unmodifiableList(r);
}
return watchedProjects;
return notificationFilters;
}
public PersonIdent newRefLogIdent() {

View File

@ -15,6 +15,7 @@
package com.google.gerrit.server;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project.NameKey;
import com.google.gerrit.server.config.AuthConfig;
@ -22,6 +23,7 @@ import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.net.SocketAddress;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ -59,7 +61,7 @@ public class PeerDaemonUser extends CurrentUser {
}
@Override
public Set<NameKey> getWatchedProjects() {
public Collection<AccountProjectWatch> getNotificationFilters() {
return Collections.emptySet();
}

View File

@ -15,12 +15,14 @@
package com.google.gerrit.server;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project.NameKey;
import com.google.gerrit.server.config.AuthConfig;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Set;
@ -73,7 +75,7 @@ public class ReplicationUser extends CurrentUser {
}
@Override
public Set<NameKey> getWatchedProjects() {
public Collection<AccountProjectWatch> getNotificationFilters() {
return Collections.emptySet();
}

View File

@ -132,7 +132,6 @@ public class GerritGlobalModule extends FactoryModule {
factory(PushAllProjectsOp.Factory.class);
bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
factory(MergeOp.Factory.class);
factory(ReloadSubmitQueueOp.Factory.class);
bind(FromAddressGenerator.class).toProvider(
@ -142,12 +141,6 @@ public class GerritGlobalModule extends FactoryModule {
bind(PatchSetInfoFactory.class);
bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
factory(FunctionState.Factory.class);
factory(AbandonedSender.Factory.class);
factory(CommentSender.Factory.class);
factory(MergedSender.Factory.class);
factory(MergeFailSender.Factory.class);
factory(RegisterNewEmailSender.Factory.class);
factory(ReplicationUser.Factory.class);
install(new LifecycleModule() {

View File

@ -21,9 +21,15 @@ import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.GroupControl;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.ReceiveCommits;
import com.google.gerrit.server.mail.AbandonedSender;
import com.google.gerrit.server.mail.AddReviewerSender;
import com.google.gerrit.server.mail.CommentSender;
import com.google.gerrit.server.mail.CreateChangeSender;
import com.google.gerrit.server.mail.MergeFailSender;
import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.mail.RegisterNewEmailSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.patch.PublishComments;
import com.google.gerrit.server.project.ChangeControl;
@ -41,14 +47,15 @@ public class GerritRequestModule extends FactoryModule {
RequestScoped.class);
bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
bind(AccountResolver.class);
bind(ChangeQueryBuilder.class);
bind(ChangeQueryRewriter.class);
bind(ChangeControl.Factory.class).in(SINGLETON);
bind(GroupControl.Factory.class).in(SINGLETON);
bind(ProjectControl.Factory.class).in(SINGLETON);
factory(ChangeQueryBuilder.Factory.class);
factory(ReceiveCommits.Factory.class);
factory(MergeOp.Factory.class);
// Not really per-request, but dammit, I don't know where else to
// easily park this stuff.
@ -57,5 +64,10 @@ public class GerritRequestModule extends FactoryModule {
factory(CreateChangeSender.Factory.class);
factory(PublishComments.Factory.class);
factory(ReplacePatchSetSender.Factory.class);
factory(AbandonedSender.Factory.class);
factory(CommentSender.Factory.class);
factory(MergedSender.Factory.class);
factory(MergeFailSender.Factory.class);
factory(RegisterNewEmailSender.Factory.class);
}
}

View File

@ -19,12 +19,30 @@ import static java.util.concurrent.TimeUnit.SECONDS;
import com.google.gerrit.reviewdb.Branch;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.RemotePeer;
import com.google.gerrit.server.RequestCleanup;
import com.google.gerrit.server.config.GerritRequestModule;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.inject.AbstractModule;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Key;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.Scope;
import com.google.inject.servlet.RequestScoped;
import com.jcraft.jsch.HostKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.net.SocketAddress;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;
@ -38,18 +56,47 @@ public class ChangeMergeQueue implements MergeQueue {
new HashMap<Branch.NameKey, RecheckJob>();
private final WorkQueue workQueue;
private final MergeOp.Factory opFactory;
private final Provider<MergeOp.Factory> bgFactory;
@Inject
ChangeMergeQueue(final WorkQueue wq, final MergeOp.Factory of) {
ChangeMergeQueue(final WorkQueue wq, Injector parent) {
workQueue = wq;
opFactory = of;
Injector child = parent.createChildInjector(new AbstractModule() {
@Override
protected void configure() {
bindScope(RequestScoped.class, MyScope.REQUEST);
install(new GerritRequestModule());
bind(CurrentUser.class).to(IdentifiedUser.class);
bind(IdentifiedUser.class).toProvider(new Provider<IdentifiedUser>() {
@Override
public IdentifiedUser get() {
throw new OutOfScopeException("No user on merge thread");
}
});
bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
new Provider<SocketAddress>() {
@Override
public SocketAddress get() {
throw new OutOfScopeException("No remote peer on merge thread");
}
});
bind(SshInfo.class).toInstance(new SshInfo() {
@Override
public List<HostKey> getHostKeys() {
return Collections.emptyList();
}
});
}
});
bgFactory = child.getProvider(MergeOp.Factory.class);
}
@Override
public void merge(final Branch.NameKey branch) {
public void merge(MergeOp.Factory mof, Branch.NameKey branch) {
if (start(branch)) {
mergeImpl(branch);
mergeImpl(mof, branch);
}
}
@ -127,7 +174,7 @@ public class ChangeMergeQueue implements MergeQueue {
e.needMerge = false;
}
private void mergeImpl(final Branch.NameKey branch) {
private void mergeImpl(MergeOp.Factory opFactory, Branch.NameKey branch) {
try {
opFactory.create(branch).merge();
} catch (Throwable e) {
@ -137,6 +184,26 @@ public class ChangeMergeQueue implements MergeQueue {
}
}
private void mergeImpl(Branch.NameKey branch) {
try {
MyScope ctx = new MyScope();
MyScope old = MyScope.set(ctx);
try {
try {
bgFactory.get().create(branch).merge();
} finally {
ctx.cleanup.run();
}
} finally {
MyScope.set(old);
}
} catch (Throwable e) {
log.error("Merge attempt for " + branch + " failed", e);
} finally {
finish(branch);
}
}
private synchronized void recheck(final RecheckJob e) {
final long remainingDelay = e.recheckAt - System.currentTimeMillis();
if (MILLISECONDS.convert(10, SECONDS) < remainingDelay) {
@ -194,4 +261,65 @@ public class ChangeMergeQueue implements MergeQueue {
return "recheck " + project.get() + " " + dest.getShortName();
}
}
private static class MyScope {
private static final ThreadLocal<MyScope> current =
new ThreadLocal<MyScope>();
private static MyScope getContext() {
final MyScope ctx = current.get();
if (ctx == null) {
throw new OutOfScopeException("Not in command/request");
}
return ctx;
}
static MyScope set(MyScope ctx) {
MyScope old = current.get();
current.set(ctx);
return old;
}
static final Scope REQUEST = new Scope() {
public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
return new Provider<T>() {
public T get() {
return getContext().get(key, creator);
}
@Override
public String toString() {
return String.format("%s[%s]", creator, REQUEST);
}
};
}
@Override
public String toString() {
return "MergeQueue.REQUEST";
}
};
private static final Key<RequestCleanup> RC_KEY =
Key.get(RequestCleanup.class);
private final RequestCleanup cleanup;
private final Map<Key<?>, Object> map;
MyScope() {
cleanup = new RequestCleanup();
map = new HashMap<Key<?>, Object>();
map.put(RC_KEY, cleanup);
}
synchronized <T> T get(Key<T> key, Provider<T> creator) {
@SuppressWarnings("unchecked")
T t = (T) map.get(key);
if (t == null) {
t = creator.get();
map.put(key, t);
}
return t;
}
}
}

View File

@ -1179,7 +1179,6 @@ public class MergeOp {
if (submitter != null) {
cm.setFrom(submitter.getAccountId());
}
cm.setReviewDb(schema);
cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
cm.send();
} catch (OrmException e) {
@ -1241,7 +1240,6 @@ public class MergeOp {
cm.setFrom(submitter.getAccountId());
}
}
cm.setReviewDb(schema);
cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
cm.setChangeMessage(msg);
cm.send();

View File

@ -19,7 +19,7 @@ import com.google.gerrit.reviewdb.Branch;
import java.util.concurrent.TimeUnit;
public interface MergeQueue {
void merge(Branch.NameKey branch);
void merge(MergeOp.Factory mof, Branch.NameKey branch);
void schedule(Branch.NameKey branch);

View File

@ -931,7 +931,6 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
cm = createChangeSenderFactory.create(change);
cm.setFrom(me);
cm.setPatchSet(ps, info);
cm.setReviewDb(db);
cm.addReviewers(reviewers);
cm.addExtraCC(cc);
cm.send();
@ -1234,7 +1233,6 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
cm.setFrom(me);
cm.setPatchSet(ps, result.info);
cm.setChangeMessage(result.msg);
cm.setReviewDb(db);
cm.addReviewers(reviewers);
cm.addExtraCC(cc);
cm.addReviewers(oldReviewers);
@ -1583,7 +1581,6 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
try {
final MergedSender cm = mergedSenderFactory.create(result.change);
cm.setFrom(currentUser.getAccountId());
cm.setReviewDb(db);
cm.setPatchSet(result.patchSet, result.info);
cm.setDest(new Branch.NameKey(project.getNameKey(),
result.mergedIntoRef));

View File

@ -25,8 +25,8 @@ public class AbandonedSender extends ReplyToChangeSender {
}
@Inject
public AbandonedSender(@Assisted Change c) {
super(c, "abandon");
public AbandonedSender(EmailArguments ea, @Assisted Change c) {
super(ea, c, "abandon");
}
@Override

View File

@ -15,6 +15,7 @@
package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@ -25,8 +26,9 @@ public class AddReviewerSender extends NewChangeSender {
}
@Inject
public AddReviewerSender(@Assisted Change c) {
super(c);
public AddReviewerSender(EmailArguments ea, SshInfo sshInfo,
@Assisted Change c) {
super(ea, sshInfo, c);
}
@Override

View File

@ -38,8 +38,8 @@ public class CommentSender extends ReplyToChangeSender {
private List<PatchLineComment> inlineComments = Collections.emptyList();
@Inject
public CommentSender(@Assisted Change c) {
super(c, "comment");
public CommentSender(EmailArguments ea, @Assisted Change c) {
super(ea, c, "comment");
}
public void setPatchLineComments(final List<PatchLineComment> plc) {
@ -124,7 +124,7 @@ public class CommentSender extends ReplyToChangeSender {
private Repository getRepository() {
try {
return server.openRepository(projectName);
return args.server.openRepository(projectName);
} catch (RepositoryNotFoundException e) {
return null;
}

View File

@ -19,7 +19,7 @@ import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountGroupMember;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@ -34,8 +34,9 @@ public class CreateChangeSender extends NewChangeSender {
}
@Inject
public CreateChangeSender(@Assisted Change c) {
super(c);
public CreateChangeSender(EmailArguments ea, SshInfo sshInfo,
@Assisted Change c) {
super(ea, sshInfo, c);
}
@Override
@ -46,38 +47,32 @@ public class CreateChangeSender extends NewChangeSender {
}
private void bccWatchers() {
if (db != null) {
try {
// BCC anyone else who has interest in this project's changes
//
final ProjectState ps = getProjectState();
if (ps != null) {
// Try to mark interested owners with a TO and not a BCC line.
//
final Set<Account.Id> owners = new HashSet<Account.Id>();
for (AccountGroup.Id g : getProjectOwners()) {
for (AccountGroupMember m : db.accountGroupMembers().byGroup(g)) {
owners.add(m.getAccountId());
}
}
try {
// Try to mark interested owners with a TO and not a BCC line.
//
final Set<Account.Id> owners = new HashSet<Account.Id>();
for (AccountGroup.Id g : getProjectOwners()) {
for (AccountGroupMember m : args.db.get().accountGroupMembers()
.byGroup(g)) {
owners.add(m.getAccountId());
}
}
// BCC anyone who has interest in this project's changes
//
for (final AccountProjectWatch w : getProjectWatches()) {
if (w.isNotifyNewChanges()) {
if (owners.contains(w.getAccountId())) {
add(RecipientType.TO, w.getAccountId());
} else {
add(RecipientType.BCC, w.getAccountId());
}
}
// BCC anyone who has interest in this project's changes
//
for (final AccountProjectWatch w : getWatches()) {
if (w.isNotifyNewChanges()) {
if (owners.contains(w.getAccountId())) {
add(RecipientType.TO, w.getAccountId());
} else {
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.
}
} 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.
}
}
}

View File

@ -0,0 +1,75 @@
// Copyright (C) 2010 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.mail;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb;
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.config.CanonicalWebUrl;
import com.google.gerrit.server.config.WildProjectName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.query.change.ChangeQueryBuilder;
import com.google.gerrit.server.query.change.ChangeQueryRewriter;
import com.google.inject.Inject;
import com.google.inject.Provider;
import javax.annotation.Nullable;
class EmailArguments {
final GitRepositoryManager server;
final ProjectCache projectCache;
final AccountCache accountCache;
final PatchListCache patchListCache;
final FromAddressGenerator fromAddressGenerator;
final EmailSender emailSender;
final PatchSetInfoFactory patchSetInfoFactory;
final IdentifiedUser.GenericFactory identifiedUserFactory;
final Provider<String> urlProvider;
final Project.NameKey wildProject;
final ChangeQueryBuilder.Factory queryBuilder;
final Provider<ChangeQueryRewriter> queryRewriter;
final Provider<ReviewDb> db;
@Inject
EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
AccountCache accountCache, PatchListCache patchListCache,
FromAddressGenerator fromAddressGenerator, EmailSender emailSender,
PatchSetInfoFactory patchSetInfoFactory,
GenericFactory identifiedUserFactory,
@CanonicalWebUrl @Nullable Provider<String> urlProvider,
@WildProjectName Project.NameKey wildProject,
ChangeQueryBuilder.Factory queryBuilder,
Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db) {
this.server = server;
this.projectCache = projectCache;
this.accountCache = accountCache;
this.patchListCache = patchListCache;
this.fromAddressGenerator = fromAddressGenerator;
this.emailSender = emailSender;
this.patchSetInfoFactory = patchSetInfoFactory;
this.identifiedUserFactory = identifiedUserFactory;
this.urlProvider = urlProvider;
this.wildProject = wildProject;
this.queryBuilder = queryBuilder;
this.queryRewriter = queryRewriter;
this.db = db;
}
}

View File

@ -25,8 +25,8 @@ public class MergeFailSender extends ReplyToChangeSender {
}
@Inject
public MergeFailSender(@Assisted Change c) {
super(c, "comment");
public MergeFailSender(EmailArguments ea, @Assisted Change c) {
super(ea, c, "comment");
}
@Override

View File

@ -37,15 +37,14 @@ public class MergedSender extends ReplyToChangeSender {
public MergedSender create(Change change);
}
private final ApprovalTypes approvalTypes;
private Branch.NameKey dest;
@Inject
private ApprovalTypes approvalTypes;
@Inject
public MergedSender(@Assisted Change c) {
super(c, "merged");
public MergedSender(EmailArguments ea, ApprovalTypes at, @Assisted Change c) {
super(ea, c, "merged");
dest = c.getDest();
approvalTypes = at;
}
public void setDest(final Branch.NameKey key) {
@ -78,7 +77,7 @@ public class MergedSender extends ReplyToChangeSender {
}
private void formatApprovals() {
if (db != null && patchSet != null) {
if (patchSet != null) {
try {
final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> pos =
new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
@ -86,8 +85,8 @@ public class MergedSender extends ReplyToChangeSender {
final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> neg =
new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
for (PatchSetApproval ca : db.patchSetApprovals().byPatchSet(
patchSet.getId())) {
for (PatchSetApproval ca : args.db.get().patchSetApprovals()
.byPatchSet(patchSet.getId())) {
if (ca.getValue() > 0) {
insert(pos, ca);
} else if (ca.getValue() < 0) {
@ -157,23 +156,18 @@ public class MergedSender extends ReplyToChangeSender {
}
private void bccWatchesNotifySubmittedChanges() {
if (db != null) {
try {
// BCC anyone else who has interest in this project's changes
//
final ProjectState ps = getProjectState();
if (ps != null) {
for (final AccountProjectWatch w : getProjectWatches()) {
if (w.isNotifySubmittedChanges()) {
add(RecipientType.BCC, w.getAccountId());
}
}
try {
// BCC anyone else who has interest in this project's changes
//
for (final AccountProjectWatch w : getWatches()) {
if (w.isNotifySubmittedChanges()) {
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.
}
} 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.
}
}
}

View File

@ -17,7 +17,6 @@ package com.google.gerrit.server.mail;
import com.google.gerrit.reviewdb.Account;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.server.ssh.SshInfo;
import com.google.inject.Inject;
import com.jcraft.jsch.HostKey;
@ -29,14 +28,13 @@ import java.util.Set;
/** Sends an email alerting a user to a new change for them to review. */
public abstract class NewChangeSender extends OutgoingEmail {
@Inject
private SshInfo sshInfo;
private final SshInfo sshInfo;
private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
protected NewChangeSender(Change c) {
super(c, "newchange");
protected NewChangeSender(EmailArguments ea, SshInfo sshInfo, Change c) {
super(ea, c, "newchange");
this.sshInfo = sshInfo;
}
public void addReviewers(final Collection<Account.Id> cc) {

View File

@ -22,27 +22,20 @@ import com.google.gerrit.reviewdb.ChangeMessage;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.PatchSetInfo;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.StarredChange;
import com.google.gerrit.reviewdb.UserIdentity;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.AccountCache;
import com.google.gerrit.server.account.AccountState;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.WildProjectName;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.mail.EmailHeader.AddressList;
import com.google.gerrit.server.patch.PatchList;
import com.google.gerrit.server.patch.PatchListCache;
import com.google.gerrit.server.patch.PatchListEntry;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.query.Predicate;
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.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.util.SystemReader;
import org.slf4j.Logger;
@ -63,8 +56,6 @@ import java.util.Random;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.Nullable;
/** Sends an email to one or more interested parties. */
public abstract class OutgoingEmail {
private static final Logger log = LoggerFactory.getLogger(OutgoingEmail.class);
@ -83,55 +74,24 @@ public abstract class OutgoingEmail {
private StringBuilder body;
private boolean inFooter;
protected final EmailArguments args;
protected Account.Id fromId;
protected PatchSet patchSet;
protected PatchSetInfo patchSetInfo;
protected ChangeMessage changeMessage;
protected ReviewDb db;
@Inject
protected GitRepositoryManager server;
@Inject
private ProjectCache projectCache;
@Inject
private AccountCache accountCache;
@Inject
private PatchListCache patchListCache;
@Inject
private FromAddressGenerator fromAddressGenerator;
@Inject
private EmailSender emailSender;
@Inject
private PatchSetInfoFactory patchSetInfoFactory;
@Inject
private IdentifiedUser.GenericFactory identifiedUserFactory;
@Inject
@CanonicalWebUrl
@Nullable
private Provider<String> urlProvider;
@Inject
@WildProjectName
private Project.NameKey wildProject;
private ProjectState projectState;
private ChangeData changeData;
protected OutgoingEmail(final Change c, final String mc) {
protected OutgoingEmail(EmailArguments ea, final Change c, final String mc) {
args = ea;
change = c;
messageClass = mc;
headers = new LinkedHashMap<String, EmailHeader>();
}
protected OutgoingEmail(final String mc) {
this(null, mc);
protected OutgoingEmail(EmailArguments ea, final String mc) {
this(ea, null, mc);
}
public void setFrom(final Account.Id id) {
@ -151,17 +111,13 @@ public abstract class OutgoingEmail {
changeMessage = cm;
}
public void setReviewDb(final ReviewDb d) {
db = d;
}
/**
* Format and enqueue the message for delivery.
*
* @throws EmailException
*/
public void send() throws EmailException {
if (!emailSender.isEnabled()) {
if (!args.emailSender.isEnabled()) {
// Server has explicitly disabled email sending.
//
return;
@ -171,7 +127,7 @@ public abstract class OutgoingEmail {
format();
if (shouldSendMessage()) {
if (fromId != null) {
final Account fromUser = accountCache.get(fromId).getAccount();
final Account fromUser = args.accountCache.get(fromId).getAccount();
if (fromUser.getGeneralPreferences().isCopySelfOnEmails()) {
// If we are impersonating a user, make sure they receive a CC of
@ -226,24 +182,22 @@ public abstract class OutgoingEmail {
appendText("Gerrit-Branch: " + change.getDest().getShortName() + "\n");
appendText("Gerrit-Owner: " + getNameEmailFor(change.getOwner()) + "\n");
if (db != null) {
try {
HashSet<Account.Id> reviewers = new HashSet<Account.Id>();
for (PatchSetApproval p : db.patchSetApprovals().byChange(
change.getId())) {
reviewers.add(p.getAccountId());
}
TreeSet<String> names = new TreeSet<String>();
for (Account.Id who : reviewers) {
names.add(getNameEmailFor(who));
}
for (String name : names) {
appendText("Gerrit-Reviewer: " + name + "\n");
}
} catch (OrmException e) {
try {
HashSet<Account.Id> reviewers = new HashSet<Account.Id>();
for (PatchSetApproval p : args.db.get().patchSetApprovals().byChange(
change.getId())) {
reviewers.add(p.getAccountId());
}
TreeSet<String> names = new TreeSet<String>();
for (Account.Id who : reviewers) {
names.add(getNameEmailFor(who));
}
for (String name : names) {
appendText("Gerrit-Reviewer: " + name + "\n");
}
} catch (OrmException e) {
}
}
@ -259,7 +213,7 @@ public abstract class OutgoingEmail {
setHeader("Message-ID", rndid.toString());
}
emailSender.send(smtpFromAddress, smtpRcptTo, headers, body.toString());
args.emailSender.send(smtpFromAddress, smtpRcptTo, headers, body.toString());
}
}
@ -268,16 +222,18 @@ public abstract class OutgoingEmail {
/** Setup the message headers and envelope (TO, CC, BCC). */
protected void init() {
if (change != null && projectCache != null) {
projectState = projectCache.get(change.getProject());
if (change != null && args.projectCache != null) {
changeData = new ChangeData(change);
projectState = args.projectCache.get(change.getProject());
projectName =
projectState != null ? projectState.getProject().getName() : null;
} else {
changeData = null;
projectState = null;
projectName = null;
}
smtpFromAddress = fromAddressGenerator.from(fromId);
smtpFromAddress = args.fromAddressGenerator.from(fromId);
if (changeMessage != null && changeMessage.getWrittenOn() != null) {
setHeader("Date", new Date(changeMessage.getWrittenOn().getTime()));
} else {
@ -313,8 +269,8 @@ public abstract class OutgoingEmail {
body = new StringBuilder();
inFooter = false;
if (fromId != null && fromAddressGenerator.isGenericAddress(fromId)) {
final Account account = accountCache.get(fromId).getAccount();
if (fromId != null && args.fromAddressGenerator.isGenericAddress(fromId)) {
final Account account = args.accountCache.get(fromId).getAccount();
final String name = account.getFullName();
final String email = account.getPreferredEmail();
@ -331,10 +287,10 @@ public abstract class OutgoingEmail {
}
}
if (change != null && db != null) {
if (change != null) {
if (patchSet == null) {
try {
patchSet = db.patchSets().get(change.currentPatchSetId());
patchSet = args.db.get().patchSets().get(change.currentPatchSetId());
} catch (OrmException err) {
patchSet = null;
}
@ -342,7 +298,7 @@ public abstract class OutgoingEmail {
if (patchSet != null && patchSetInfo == null) {
try {
patchSetInfo = patchSetInfoFactory.get(patchSet.getId());
patchSetInfo = args.patchSetInfoFactory.get(patchSet.getId());
} catch (PatchSetInfoNotAvailableException err) {
patchSetInfo = null;
}
@ -439,7 +395,7 @@ public abstract class OutgoingEmail {
}
protected String getGerritUrl() {
return urlProvider.get();
return args.urlProvider.get();
}
protected String getChangeMessageThreadId() {
@ -521,7 +477,7 @@ public abstract class OutgoingEmail {
/** Get the patch list corresponding to this patch set. */
protected PatchList getPatchList() {
if (patchSet != null) {
return patchListCache.get(change, patchSet);
return args.patchListCache.get(change, patchSet);
}
return null;
}
@ -532,7 +488,7 @@ public abstract class OutgoingEmail {
return "Anonymous Coward";
}
final Account userAccount = accountCache.get(accountId).getAccount();
final Account userAccount = args.accountCache.get(accountId).getAccount();
String name = userAccount.getFullName();
if (name == null) {
name = userAccount.getPreferredEmail();
@ -544,7 +500,7 @@ public abstract class OutgoingEmail {
}
private String getNameEmailFor(Account.Id accountId) {
AccountState who = accountCache.get(accountId);
AccountState who = args.accountCache.get(accountId);
String name = who.getAccount().getFullName();
String email = who.getAccount().getPreferredEmail();
@ -594,7 +550,7 @@ public abstract class OutgoingEmail {
protected Set<AccountGroup.Id> getProjectOwners() {
final ProjectState r;
r = projectCache.get(change.getProject());
r = args.projectCache.get(change.getProject());
return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
}
@ -625,62 +581,82 @@ public abstract class OutgoingEmail {
/** BCC any user who has starred this change. */
protected void bccStarredBy() {
if (db != null) {
try {
// BCC anyone who has starred this change.
//
for (StarredChange w : db.starredChanges().byChange(change.getId())) {
add(RecipientType.BCC, w.getAccountId());
}
} catch (OrmException err) {
// 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.
try {
// BCC anyone who has starred this change.
//
for (StarredChange w : args.db.get().starredChanges().byChange(
change.getId())) {
add(RecipientType.BCC, w.getAccountId());
}
} catch (OrmException err) {
// 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.
}
}
/** BCC any user who has set "notify all comments" on this project. */
protected void bccWatchesNotifyAllComments() {
if (db != null) {
try {
// BCC anyone else who has interest in this project's changes
//
final ProjectState ps = getProjectState();
if (ps != null) {
for (final AccountProjectWatch w : getProjectWatches()) {
if (w.isNotifyAllComments()) {
add(RecipientType.BCC, w.getAccountId());
}
}
try {
// BCC anyone else who has interest in this project's changes
//
for (final AccountProjectWatch w : getWatches()) {
if (w.isNotifyAllComments()) {
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.
}
} 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.
}
}
/** Returns all watches that are relevant for this project */
final protected Set<AccountProjectWatch> getProjectWatches() throws OrmException {
final Set<AccountProjectWatch> projectWatches = new HashSet<AccountProjectWatch>();
final Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
final ProjectState ps = getProjectState();
if (ps != null) {
for (final AccountProjectWatch w : db.accountProjectWatches().byProject(ps.getProject().getNameKey())) {
projectWatches.add(w);
projectWatchers.add(w.getAccountId());
}
/** Returns all watches that are relevant */
protected final List<AccountProjectWatch> getWatches() throws OrmException {
if (changeData == null) {
return Collections.emptyList();
}
for (final AccountProjectWatch w : db.accountProjectWatches().byProject(wildProject)) {
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.wildProject)) {
if (!projectWatchers.contains(w.getAccountId())) {
// the all projects watch settings are only relevant if the user did not configure
// any specific rules for the concrete project
projectWatches.add(w);
add(matching, w);
}
}
return Collections.unmodifiableSet(projectWatches);
return Collections.unmodifiableList(matching);
}
@SuppressWarnings("unchecked")
private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
throws OrmException {
IdentifiedUser user =
args.identifiedUserFactory.create(args.db, w.getAccountId());
ChangeQueryBuilder qb = args.queryBuilder.create(user);
Predicate<ChangeData> p = qb.is_visible();
if (w.getFilter() != null) {
try {
p = Predicate.and(qb.parse(w.getFilter()), p);
p = args.queryRewriter.get().rewrite(p);
if (p.match(changeData)) {
matching.add(w);
}
} catch (QueryParseException e) {
// Ignore broken filter expressions.
}
} else if (p.match(changeData)) {
matching.add(w);
}
}
/** Any user who has published comments on this change. */
@ -694,19 +670,17 @@ public abstract class OutgoingEmail {
}
private void ccApprovals(final boolean includeZero) {
if (db != null) {
try {
// CC anyone else who has posted an approval mark on this change
//
for (PatchSetApproval ap : db.patchSetApprovals().byChange(
change.getId())) {
if (!includeZero && ap.getValue() == 0) {
continue;
}
add(RecipientType.CC, ap.getAccountId());
try {
// CC anyone else who has posted an approval mark on this change
//
for (PatchSetApproval ap : args.db.get().patchSetApprovals().byChange(
change.getId())) {
if (!includeZero && ap.getValue() == 0) {
continue;
}
} catch (OrmException err) {
add(RecipientType.CC, ap.getAccountId());
}
} catch (OrmException err) {
}
}
@ -721,14 +695,14 @@ public abstract class OutgoingEmail {
private boolean isVisibleTo(final Account.Id to) {
return projectState == null
|| change == null
|| projectState.controlFor(identifiedUserFactory.create(to))
|| projectState.controlFor(args.identifiedUserFactory.create(to))
.controlFor(change).isVisible();
}
/** Schedule delivery of this message to the given account. */
protected void add(final RecipientType rt, final Address addr) {
if (addr != null && addr.email != null && addr.email.length() > 0) {
if (emailSender.canEmail(addr.email)) {
if (args.emailSender.canEmail(addr.email)) {
smtpRcptTo.add(addr);
switch (rt) {
case TO:
@ -745,7 +719,7 @@ public abstract class OutgoingEmail {
}
private Address toAddress(final Account.Id id) {
final Account a = accountCache.get(id).getAccount();
final Account a = args.accountCache.get(id).getAccount();
final String e = a.getPreferredEmail();
if (e == null) {
return null;

View File

@ -28,14 +28,14 @@ public class RegisterNewEmailSender extends OutgoingEmail {
public RegisterNewEmailSender create(String address);
}
private final AuthConfig authConfig;
private final String addr;
@Inject
private AuthConfig authConfig;
@Inject
public RegisterNewEmailSender(@Assisted final String address) {
super("registernewemail");
public RegisterNewEmailSender(EmailArguments ea, AuthConfig ac,
@Assisted final String address) {
super(ea, "registernewemail");
authConfig = ac;
addr = address;
}

View File

@ -34,15 +34,14 @@ public class ReplacePatchSetSender extends ReplyToChangeSender {
public ReplacePatchSetSender create(Change change);
}
@Inject
private SshInfo sshInfo;
private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
private final SshInfo sshInfo;
@Inject
public ReplacePatchSetSender(@Assisted Change c) {
super(c, "newpatchset");
public ReplacePatchSetSender(EmailArguments ea, SshInfo si, @Assisted Change c) {
super(ea, c, "newpatchset");
sshInfo = si;
}
public void addReviewers(final Collection<Account.Id> cc) {

View File

@ -18,8 +18,8 @@ import com.google.gerrit.reviewdb.Change;
/** Alert a user to a reply to a change, usually commentary made during review. */
public abstract class ReplyToChangeSender extends OutgoingEmail {
protected ReplyToChangeSender(Change c, String mc) {
super(c, mc);
protected ReplyToChangeSender(EmailArguments ea, Change c, String mc) {
super(ea, c, mc);
}
@Override

View File

@ -284,7 +284,6 @@ public class PublishComments implements Callable<VoidResult> {
cm.setPatchSet(patchSet, patchSetInfoFactory.get(patchSetId));
cm.setChangeMessage(message);
cm.setPatchLineComments(drafts);
cm.setReviewDb(db);
cm.send();
} catch (EmailException e) {
log.error("Cannot send comments by email for patch set " + patchSetId, e);

View File

@ -26,8 +26,6 @@ import static com.google.gerrit.server.query.QueryParser.OR;
import static com.google.gerrit.server.query.QueryParser.SINGLE_WORD;
import static com.google.gerrit.server.query.QueryParser.VARIABLE_ASSIGN;
import com.google.gerrit.server.query.change.ChangeData;
import org.antlr.runtime.tree.Tree;
import java.lang.annotation.ElementType;
@ -239,6 +237,29 @@ public abstract class QueryBuilder<T> {
throw error("Unsupported query:" + value);
}
/**
* Locate a predicate in the predicate tree.
*
* @param p the predicate to find.
* @param clazz type of the predicate instance.
* @return the predicate, null if not found.
*/
@SuppressWarnings("unchecked")
public <P extends Predicate<T>> P find(Predicate<T> p, Class<P> clazz) {
if (clazz.isAssignableFrom(p.getClass())) {
return (P) p;
}
for (Predicate<T> c : p.getChildren()) {
P r = find(c, clazz);
if (r != null) {
return r;
}
}
return null;
}
/**
* Locate a predicate in the predicate tree.
*

View File

@ -35,7 +35,7 @@ import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.servlet.RequestScoped;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.lib.AbbreviatedObjectId;
@ -46,7 +46,6 @@ import java.util.regex.Pattern;
/**
* Parses a query string meant to be applied to change objects.
*/
@RequestScoped
public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
private static final Pattern PAT_LEGACY_ID = Pattern.compile("^[1-9][0-9]*$");
private static final Pattern PAT_CHANGE_ID =
@ -86,55 +85,67 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
new QueryBuilder.Definition<ChangeData, ChangeQueryBuilder>(
ChangeQueryBuilder.class);
private final Provider<ReviewDb> dbProvider;
private final Provider<CurrentUser> currentUser;
private final IdentifiedUser.GenericFactory userFactory;
private final ChangeControl.Factory changeControlFactory;
private final AccountResolver accountResolver;
private final GroupCache groupCache;
private final AuthConfig authConfig;
private final ApprovalTypes approvalTypes;
private final Project.NameKey wildProjectName;
static class Arguments {
final Provider<ReviewDb> dbProvider;
final Provider<ChangeQueryRewriter> rewriter;
final IdentifiedUser.GenericFactory userFactory;
final ChangeControl.Factory changeControlFactory;
final AccountResolver accountResolver;
final GroupCache groupCache;
final AuthConfig authConfig;
final ApprovalTypes approvalTypes;
final Project.NameKey wildProjectName;
@Inject
ChangeQueryBuilder(Provider<ReviewDb> dbProvider,
Provider<CurrentUser> currentUser,
IdentifiedUser.GenericFactory userFactory,
ChangeControl.Factory changeControlFactory,
AccountResolver accountResolver, GroupCache groupCache,
AuthConfig authConfig, ApprovalTypes approvalTypes,
@WildProjectName Project.NameKey wildProjectName) {
super(mydef);
this.dbProvider = dbProvider;
this.currentUser = currentUser;
this.userFactory = userFactory;
this.changeControlFactory = changeControlFactory;
this.accountResolver = accountResolver;
this.groupCache = groupCache;
this.authConfig = authConfig;
this.approvalTypes = approvalTypes;
this.wildProjectName = wildProjectName;
@Inject
Arguments(Provider<ReviewDb> dbProvider,
Provider<ChangeQueryRewriter> rewriter,
IdentifiedUser.GenericFactory userFactory,
ChangeControl.Factory changeControlFactory,
AccountResolver accountResolver, GroupCache groupCache,
AuthConfig authConfig, ApprovalTypes approvalTypes,
@WildProjectName Project.NameKey wildProjectName) {
this.dbProvider = dbProvider;
this.rewriter = rewriter;
this.userFactory = userFactory;
this.changeControlFactory = changeControlFactory;
this.accountResolver = accountResolver;
this.groupCache = groupCache;
this.authConfig = authConfig;
this.approvalTypes = approvalTypes;
this.wildProjectName = wildProjectName;
}
}
Provider<ReviewDb> getReviewDbProvider() {
return dbProvider;
public interface Factory {
ChangeQueryBuilder create(CurrentUser user);
}
private final Arguments args;
private final CurrentUser currentUser;
@Inject
ChangeQueryBuilder(Arguments args, @Assisted CurrentUser currentUser) {
super(mydef);
this.args = args;
this.currentUser = currentUser;
}
@Operator
public Predicate<ChangeData> age(String value) {
return new AgePredicate(dbProvider, value);
return new AgePredicate(args.dbProvider, value);
}
@Operator
public Predicate<ChangeData> change(String query) {
if (PAT_LEGACY_ID.matcher(query).matches()) {
return new LegacyChangeIdPredicate(dbProvider, Change.Id.parse(query));
return new LegacyChangeIdPredicate(args.dbProvider, Change.Id
.parse(query));
} else if (PAT_CHANGE_ID.matcher(query).matches()) {
if (query.charAt(0) == 'i') {
query = "I" + query.substring(1);
}
return new ChangeIdPredicate(dbProvider, query);
return new ChangeIdPredicate(args.dbProvider, query);
}
throw new IllegalArgumentException();
@ -146,30 +157,30 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
return status_open();
} else if ("closed".equals(statusName)) {
return ChangeStatusPredicate.closed(dbProvider);
return ChangeStatusPredicate.closed(args.dbProvider);
} else if ("reviewed".equalsIgnoreCase(statusName)) {
return new IsReviewedPredicate(dbProvider);
return new IsReviewedPredicate(args.dbProvider);
} else {
return new ChangeStatusPredicate(dbProvider, statusName);
return new ChangeStatusPredicate(args.dbProvider, statusName);
}
}
public Predicate<ChangeData> status_open() {
return ChangeStatusPredicate.open(dbProvider);
return ChangeStatusPredicate.open(args.dbProvider);
}
@Operator
public Predicate<ChangeData> has(String value) {
if ("star".equalsIgnoreCase(value)) {
return new IsStarredByPredicate(dbProvider, currentUser.get());
return new IsStarredByPredicate(args.dbProvider, currentUser);
}
if ("draft".equalsIgnoreCase(value)) {
if (currentUser.get() instanceof IdentifiedUser) {
return new HasDraftByPredicate(dbProvider,
((IdentifiedUser) currentUser.get()).getAccountId());
if (currentUser instanceof IdentifiedUser) {
return new HasDraftByPredicate(args.dbProvider,
((IdentifiedUser) currentUser).getAccountId());
}
}
@ -179,21 +190,19 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
@Operator
public Predicate<ChangeData> is(String value) {
if ("starred".equalsIgnoreCase(value)) {
return new IsStarredByPredicate(dbProvider, currentUser.get());
return new IsStarredByPredicate(args.dbProvider, currentUser);
}
if ("watched".equalsIgnoreCase(value)) {
return new IsWatchedByPredicate(dbProvider, wildProjectName, //
currentUser.get());
return new IsWatchedByPredicate(args, currentUser);
}
if ("visible".equalsIgnoreCase(value)) {
return new IsVisibleToPredicate(dbProvider, changeControlFactory,
currentUser.get());
return is_visible();
}
if ("reviewed".equalsIgnoreCase(value)) {
return new IsReviewedPredicate(dbProvider);
return new IsReviewedPredicate(args.dbProvider);
}
try {
@ -207,121 +216,129 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
@Operator
public Predicate<ChangeData> commit(String id) {
return new CommitPredicate(dbProvider, AbbreviatedObjectId.fromString(id));
return new CommitPredicate(args.dbProvider, AbbreviatedObjectId
.fromString(id));
}
@Operator
public Predicate<ChangeData> project(String name) {
return new ProjectPredicate(dbProvider, name);
return new ProjectPredicate(args.dbProvider, name);
}
@Operator
public Predicate<ChangeData> branch(String name) {
return new BranchPredicate(dbProvider, name);
return new BranchPredicate(args.dbProvider, name);
}
@Operator
public Predicate<ChangeData> topic(String name) {
return new TopicPredicate(dbProvider, name);
return new TopicPredicate(args.dbProvider, name);
}
@Operator
public Predicate<ChangeData> ref(String ref) {
return new RefPredicate(dbProvider, ref);
return new RefPredicate(args.dbProvider, ref);
}
@Operator
public Predicate<ChangeData> label(String name) {
return new LabelPredicate(dbProvider, approvalTypes, name);
return new LabelPredicate(args.dbProvider, args.approvalTypes, name);
}
@Operator
public Predicate<ChangeData> starredby(String who)
throws QueryParseException, OrmException {
Account account = accountResolver.find(who);
Account account = args.accountResolver.find(who);
if (account == null) {
throw error("User " + who + " not found");
}
return new IsStarredByPredicate(dbProvider, //
userFactory.create(dbProvider, account.getId()));
return new IsStarredByPredicate(args.dbProvider, //
args.userFactory.create(args.dbProvider, account.getId()));
}
@Operator
public Predicate<ChangeData> watchedby(String who)
throws QueryParseException, OrmException {
Account account = accountResolver.find(who);
Account account = args.accountResolver.find(who);
if (account == null) {
throw error("User " + who + " not found");
}
return new IsWatchedByPredicate(dbProvider, wildProjectName, //
userFactory.create(dbProvider, account.getId()));
return new IsWatchedByPredicate(args, args.userFactory.create(
args.dbProvider, account.getId()));
}
@Operator
public Predicate<ChangeData> draftby(String who) throws QueryParseException,
OrmException {
Account account = accountResolver.find(who);
Account account = args.accountResolver.find(who);
if (account == null) {
throw error("User " + who + " not found");
}
return new HasDraftByPredicate(dbProvider, account.getId());
return new HasDraftByPredicate(args.dbProvider, account.getId());
}
@Operator
public Predicate<ChangeData> visibleto(String who)
throws QueryParseException, OrmException {
Account account = accountResolver.find(who);
Account account = args.accountResolver.find(who);
if (account != null) {
return visibleto(userFactory.create(dbProvider, account.getId()));
return visibleto(args.userFactory
.create(args.dbProvider, account.getId()));
}
// If its not an account, maybe its a group?
//
AccountGroup g = groupCache.get(new AccountGroup.NameKey(who));
AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
if (g != null) {
return visibleto(new SingleGroupUser(authConfig, g.getId()));
return visibleto(new SingleGroupUser(args.authConfig, g.getId()));
}
Collection<AccountGroup> matches =
groupCache.get(new AccountGroup.ExternalNameKey(who));
args.groupCache.get(new AccountGroup.ExternalNameKey(who));
if (matches != null && !matches.isEmpty()) {
HashSet<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
for (AccountGroup group : matches) {
ids.add(group.getId());
}
return visibleto(new SingleGroupUser(authConfig, ids));
return visibleto(new SingleGroupUser(args.authConfig, ids));
}
throw error("No user or group matches \"" + who + "\".");
}
public Predicate<ChangeData> visibleto(CurrentUser user) {
return new IsVisibleToPredicate(dbProvider, changeControlFactory, user);
return new IsVisibleToPredicate(args.dbProvider, //
args.changeControlFactory, //
user);
}
public Predicate<ChangeData> is_visible() {
return visibleto(currentUser);
}
@Operator
public Predicate<ChangeData> owner(String who) throws QueryParseException,
OrmException {
Account account = accountResolver.find(who);
Account account = args.accountResolver.find(who);
if (account == null) {
throw error("User " + who + " not found");
}
return new OwnerPredicate(dbProvider, account.getId());
return new OwnerPredicate(args.dbProvider, account.getId());
}
@Operator
public Predicate<ChangeData> reviewer(String nameOrEmail)
throws QueryParseException, OrmException {
Account account = accountResolver.find(nameOrEmail);
Account account = args.accountResolver.find(nameOrEmail);
if (account == null) {
throw error("Reviewer " + nameOrEmail + " not found");
}
return new ReviewerPredicate(dbProvider, account.getId());
return new ReviewerPredicate(args.dbProvider, account.getId());
}
@Operator
public Predicate<ChangeData> tr(String trackingId) {
return new TrackingIdPredicate(dbProvider, trackingId);
return new TrackingIdPredicate(args.dbProvider, trackingId);
}
@Operator
@ -350,12 +367,12 @@ public class ChangeQueryBuilder extends QueryBuilder<ChangeData> {
@Operator
public Predicate<ChangeData> sortkey_after(String sortKey) {
return new SortKeyPredicate.After(dbProvider, sortKey);
return new SortKeyPredicate.After(args.dbProvider, sortKey);
}
@Operator
public Predicate<ChangeData> sortkey_before(String sortKey) {
return new SortKeyPredicate.Before(dbProvider, sortKey);
return new SortKeyPredicate.Before(args.dbProvider, sortKey);
}
@Operator

View File

@ -18,7 +18,6 @@ import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.ChangeAccess;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.query.IntPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryRewriter;
@ -29,18 +28,18 @@ import com.google.inject.Inject;
import com.google.inject.OutOfScopeException;
import com.google.inject.Provider;
import com.google.inject.name.Named;
import com.google.inject.servlet.RequestScoped;
import java.util.Collection;
@RequestScoped
public class ChangeQueryRewriter extends QueryRewriter<ChangeData> {
private static final QueryRewriter.Definition<ChangeData, ChangeQueryRewriter> mydef =
new QueryRewriter.Definition<ChangeData, ChangeQueryRewriter>(
ChangeQueryRewriter.class, new ChangeQueryBuilder(
new InvalidProvider<ReviewDb>(),
new InvalidProvider<CurrentUser>(), //
null, null, null, null, null, null, null));
new ChangeQueryBuilder.Arguments( //
new InvalidProvider<ReviewDb>(), //
new InvalidProvider<ChangeQueryRewriter>(), //
null, null, null, null, null, null, null),
null));
private final Provider<ReviewDb> dbProvider;

View File

@ -14,18 +14,20 @@
package com.google.gerrit.server.query.change;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.Project.NameKey;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.WildProjectName;
import com.google.gerrit.server.query.OperatorPredicate;
import com.google.gerrit.server.query.Predicate;
import com.google.gerrit.server.query.QueryParseException;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Provider;
import java.util.Set;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
class IsWatchedByPredicate extends OperatorPredicate<ChangeData> {
private static String describe(CurrentUser user) {
@ -35,32 +37,81 @@ class IsWatchedByPredicate extends OperatorPredicate<ChangeData> {
return user.toString();
}
private final Provider<ReviewDb> db;
private final Project.NameKey wildProject;
private final ChangeQueryBuilder.Arguments args;
private final CurrentUser user;
IsWatchedByPredicate(Provider<ReviewDb> db,
@WildProjectName Project.NameKey wildProject, CurrentUser user) {
private Map<Project.NameKey, List<Predicate<ChangeData>>> rules;
IsWatchedByPredicate(ChangeQueryBuilder.Arguments args, CurrentUser user) {
super(ChangeQueryBuilder.FIELD_WATCHEDBY, describe(user));
this.db = db;
this.wildProject = wildProject;
this.args = args;
this.user = user;
}
@Override
public boolean match(final ChangeData cd) throws OrmException {
Set<NameKey> watched = user.getWatchedProjects();
if (watched.contains(wildProject)) {
return true;
if (rules == null) {
ChangeQueryBuilder builder = new ChangeQueryBuilder(args, user);
rules = new HashMap<Project.NameKey, List<Predicate<ChangeData>>>();
for (AccountProjectWatch w : user.getNotificationFilters()) {
List<Predicate<ChangeData>> list = rules.get(w.getProjectNameKey());
if (list == null) {
list = new ArrayList<Predicate<ChangeData>>(4);
rules.put(w.getProjectNameKey(), list);
}
Predicate<ChangeData> p = compile(builder, w);
if (p != null) {
list.add(p);
}
}
}
Change change = cd.change(db);
if (rules.isEmpty()) {
return false;
}
Change change = cd.change(args.dbProvider);
if (change == null) {
return false;
}
Project.NameKey project = change.getDest().getParentKey();
return watched.contains(project);
List<Predicate<ChangeData>> list = rules.get(project);
if (list == null) {
list = rules.get(args.wildProjectName);
}
if (list != null) {
for (Predicate<ChangeData> p : list) {
if (p.match(cd)) {
return true;
}
}
}
return false;
}
@SuppressWarnings("unchecked")
private Predicate<ChangeData> compile(ChangeQueryBuilder builder,
AccountProjectWatch w) {
Predicate<ChangeData> p = builder.is_visible();
if (w.getFilter() != null) {
try {
p = Predicate.and(builder.parse(w.getFilter()), p);
if (builder.find(p, IsWatchedByPredicate.class) != null) {
// If the query is going to infinite loop, assume it
// will never match and return null. Yes this test
// prevents you from having a filter that matches what
// another user is filtering on. :-)
//
return null;
}
p = args.rewriter.get().rewrite(p);
} catch (QueryParseException e) {
return null;
}
}
return p;
}
@Override

View File

@ -60,7 +60,6 @@ public class QueryProcessor {
private final SimpleDateFormat sdf =
new SimpleDateFormat("yyyy-MM-dd HH:mm:ss zzz");
private final CurrentUser currentUser;
private final EventFactory eventFactory;
private final ChangeQueryBuilder queryBuilder;
private final ChangeQueryRewriter queryRewriter;
@ -75,12 +74,11 @@ public class QueryProcessor {
private PrintWriter out;
@Inject
QueryProcessor(CurrentUser currentUser, EventFactory eventFactory,
ChangeQueryBuilder queryBuilder, ChangeQueryRewriter queryRewriter,
Provider<ReviewDb> db) {
this.currentUser = currentUser;
QueryProcessor(EventFactory eventFactory,
ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db) {
this.eventFactory = eventFactory;
this.queryBuilder = queryBuilder;
this.queryBuilder = queryBuilder.create(currentUser);
this.queryRewriter = queryRewriter;
this.db = db;
}
@ -107,9 +105,7 @@ public class QueryProcessor {
final QueryStats stats = new QueryStats();
stats.runTimeMilliseconds = System.currentTimeMillis();
final Predicate<ChangeData> visibleToMe =
queryBuilder.visibleto(currentUser);
final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
Predicate<ChangeData> s = compileQuery(queryString, visibleToMe);
List<ChangeData> results = new ArrayList<ChangeData>();
HashSet<Change.Id> want = new HashSet<Change.Id>();

View File

@ -15,12 +15,14 @@
package com.google.gerrit.server.query.change;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.server.AccessPath;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.AuthConfig;
import java.util.Collection;
import java.util.Collections;
import java.util.Set;
@ -47,7 +49,7 @@ final class SingleGroupUser extends CurrentUser {
}
@Override
public Set<Project.NameKey> getWatchedProjects() {
public Collection<AccountProjectWatch> getNotificationFilters() {
return Collections.emptySet();
}
}

View File

@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
private static final Class<? extends SchemaVersion> C = Schema_39.class;
private static final Class<? extends SchemaVersion> C = Schema_40.class;
public static class Module extends AbstractModule {
@Override

View File

@ -0,0 +1,70 @@
// Copyright (C) 2010 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.schema;
import com.google.gerrit.reviewdb.AccountProjectWatch;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.gwtorm.schema.sql.DialectH2;
import com.google.gwtorm.schema.sql.DialectMySQL;
import com.google.gwtorm.schema.sql.DialectPostgreSQL;
import com.google.gwtorm.schema.sql.SqlDialect;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.sql.SQLException;
import java.sql.Statement;
public class Schema_40 extends SchemaVersion {
@Inject
Schema_40(Provider<Schema_39> prior) {
super(prior);
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException,
OrmException {
// Set to "*" the filter field of the previously watched projects
//
Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
try {
stmt.execute("UPDATE account_project_watches" //
+ " SET filter = '" + AccountProjectWatch.FILTER_ALL + "'" //
+ " WHERE filter IS NULL OR filter = ''");
// Set the new primary key
//
final SqlDialect dialect = ((JdbcSchema) db).getDialect();
if (dialect instanceof DialectPostgreSQL) {
stmt.execute("ALTER TABLE account_project_watches "
+ "DROP CONSTRAINT account_project_watches_pkey");
stmt.execute("ALTER TABLE account_project_watches "
+ "ADD PRIMARY KEY (account_id, project_name, filter)");
} else if ((dialect instanceof DialectH2)
|| (dialect instanceof DialectMySQL)) {
stmt.execute("ALTER TABLE account_project_watches DROP PRIMARY KEY");
stmt.execute("ALTER TABLE account_project_watches "
+ "ADD PRIMARY KEY (account_id, project_name, filter)");
} else {
throw new OrmException("Unsupported dialect " + dialect);
}
} finally {
stmt.close();
}
}
}

View File

@ -25,6 +25,7 @@ import com.google.gerrit.reviewdb.RevId;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.git.MergeOp;
import com.google.gerrit.server.git.MergeQueue;
import com.google.gerrit.server.patch.PublishComments;
import com.google.gerrit.server.project.CanSubmitResult;
@ -95,6 +96,9 @@ public class ReviewCommand extends BaseCommand {
@Inject
private MergeQueue merger;
@Inject
private MergeOp.Factory opFactory;
@Inject
private ApprovalTypes approvalTypes;
@ -166,7 +170,7 @@ public class ReviewCommand extends BaseCommand {
changeControl.canSubmit(patchSetId, db, approvalTypes,
functionStateFactory);
if (result == CanSubmitResult.OK) {
ChangeUtil.submit(patchSetId, currentUser, db, merger);
ChangeUtil.submit(opFactory, patchSetId, currentUser, db, merger);
} else {
throw error(result.getMessage());
}