Reports gatekeeper message and potential voters.

This commit is contained in:
robin shine 2013-09-07 13:53:18 +08:00
parent 1f1f3709b9
commit 63fc7c35d2
34 changed files with 561 additions and 258 deletions

View File

@ -64,6 +64,11 @@ public class EntityPatternSet extends PatternSet implements Trimmable {
return input;
}
@Override
public String toString() {
return new PatternSet(asInput()).toString();
}
public static EntityPatternSet fromInput(List<ExclusiveAwarePattern> input, EntityLoader entityLoader) {
List<ExclusiveAwarePattern> stored = new ArrayList<ExclusiveAwarePattern>();
for (ExclusiveAwarePattern eachInput: input) {

View File

@ -15,6 +15,7 @@ import com.pmease.commons.jetty.extensionpoints.ServletContextConfigurator;
import com.pmease.commons.loader.AbstractPlugin;
import com.pmease.commons.persistence.AbstractEntity;
import com.pmease.commons.persistence.extensionpoints.ModelContribution;
import com.pmease.commons.util.ClassUtils;
import com.pmease.gitop.core.model.User;
import com.pmease.gitop.core.web.asset.AssetLocator;
@ -42,7 +43,7 @@ public class CorePlugin extends AbstractPlugin {
public Collection<Class<? extends AbstractEntity>> getModelClasses() {
Collection<Class<? extends AbstractEntity>> modelClasses =
new HashSet<Class<? extends AbstractEntity>>();
modelClasses.add(User.class);
modelClasses.addAll(ClassUtils.findSubClasses(AbstractEntity.class, User.class));
return modelClasses;
}
});

View File

@ -1,5 +1,7 @@
package com.pmease.gitop.core.gatekeeper;
import java.util.List;
import javax.annotation.Nullable;
public abstract class AbstractGateKeeper implements GateKeeper {
@ -9,4 +11,36 @@ public abstract class AbstractGateKeeper implements GateKeeper {
return this;
}
protected CheckResult accept(String reason) {
return new CheckResult.Accept(reason);
}
protected CheckResult reject(String reason) {
return new CheckResult.Reject(reason);
}
protected CheckResult pending(String reason) {
return new CheckResult.Pending(reason);
}
protected CheckResult block(String reason) {
return new CheckResult.Block(reason);
}
protected CheckResult accept(List<String> reasons) {
return new CheckResult.Accept(reasons);
}
protected CheckResult reject(List<String> reasons) {
return new CheckResult.Reject(reasons);
}
protected CheckResult pending(List<String> reasons) {
return new CheckResult.Pending(reasons);
}
protected CheckResult block(List<String> reasons) {
return new CheckResult.Block(reasons);
}
}

View File

@ -6,7 +6,7 @@ public class AlwaysAccept extends AbstractGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
return CheckResult.ACCEPT;
return accept("always");
}
}

View File

@ -7,7 +7,7 @@ import com.pmease.commons.util.trimmable.AndOrConstruct;
import com.pmease.commons.util.trimmable.TrimUtils;
import com.pmease.gitop.core.model.MergeRequest;
public class AndGateKeeper implements GateKeeper {
public class AndGateKeeper extends AbstractGateKeeper {
private List<GateKeeper> gateKeepers = new ArrayList<GateKeeper>();
@ -21,20 +21,27 @@ public class AndGateKeeper implements GateKeeper {
@Override
public CheckResult check(MergeRequest request) {
boolean pending = false;
List<String> pendingReasons = new ArrayList<String>();
List<String> acceptReasons = new ArrayList<String>();
for (GateKeeper each: getGateKeepers()) {
CheckResult result = each.check(request);
if (result == CheckResult.REJECT || result == CheckResult.PENDING_BLOCK)
if (result.isAccept()) {
acceptReasons.addAll(result.getReasons());
} else if (result.isReject()) {
return result;
else if (result == CheckResult.PENDING)
pending = true;
} else if (result.isBlock()) {
result.getReasons().addAll(pendingReasons);
return result;
} else {
pendingReasons.addAll(result.getReasons());
}
}
if (pending)
return CheckResult.PENDING;
if (!pendingReasons.isEmpty())
return pending(pendingReasons);
else
return CheckResult.ACCEPT;
return accept(acceptReasons);
}
@Override

View File

@ -20,8 +20,14 @@ public class ApprovedByAuthorizedUsers extends AbstractGateKeeper {
CheckResult result = or.check(request);
if (result.isPending()) {
request.requestVote(authorizedUsers);
if (result.isAccept()) {
result = accept("Approved by user with push permission.");
} else if (result.isReject()) {
result = reject("Not approved by any users with push permission.");
} else if (result.isPending()) {
result = pending("To be approved by someone with push permission.");
} else {
result = block("To be approved by someone with push permission.");
}
return result;
}

View File

@ -11,7 +11,14 @@ public class ApprovedByMajoritiesOfSpecifiedTeam extends TeamAwareGateKeeper {
gateKeeper.setRequireVoteOfAllMembers(true);
gateKeeper.setTeamId(getTeamId());
return gateKeeper.check(request);
CheckResult result = gateKeeper.check(request);
if (result.isAccept())
result = accept("Approved by majorities of team '" + getTeam().getName() + "'.");
else if (result.isReject())
result = reject("Not approved by majorities of team '" + getTeam().getName() + "'.");
return result;
}
}

View File

@ -3,6 +3,7 @@ package com.pmease.gitop.core.gatekeeper;
import com.pmease.commons.util.EasySet;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.User;
import com.pmease.gitop.core.model.Vote;
public class ApprovedByRepositoryOwner extends AbstractGateKeeper {
@ -10,10 +11,16 @@ public class ApprovedByRepositoryOwner extends AbstractGateKeeper {
public CheckResult check(MergeRequest request) {
User repositoryOwner = request.getDestination().getRepository().getOwner();
CheckResult result = repositoryOwner.checkApprovalSince(request.getBaseUpdate());
if (result.isPending())
request.requestVote(EasySet.of(repositoryOwner));
return result;
Vote.Result result = repositoryOwner.checkVoteSince(request.getBaseUpdate());
if (result == null) {
request.inviteToVote(EasySet.of(repositoryOwner), 1);
return pending("To be approved by user '" + repositoryOwner.getName() + "'.");
} else if (result.isAccept()) {
return accept("Approved by user '" + repositoryOwner.getName() + "'.");
} else {
return reject("Rejected by user '" + repositoryOwner.getName() + "'.");
}
}
}

View File

@ -5,12 +5,10 @@ import java.util.HashSet;
import javax.validation.constraints.Min;
import com.pmease.commons.loader.AppLoader;
import com.pmease.gitop.core.manager.TeamManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.Team;
import com.pmease.gitop.core.model.TeamMembership;
import com.pmease.gitop.core.model.User;
import com.pmease.gitop.core.model.Vote;
public class ApprovedBySpecifiedTeam extends TeamAwareGateKeeper {
@ -27,41 +25,31 @@ public class ApprovedBySpecifiedTeam extends TeamAwareGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
TeamManager teamManager = AppLoader.getInstance(TeamManager.class);
Team team = teamManager.load(getTeamId());
Collection<User> members = new HashSet<User>();
for (TeamMembership membership: team.getMemberships())
for (TeamMembership membership: getTeam().getMemberships())
members.add(membership.getUser());
boolean pendingBlock = false;
int approvals = 0;
int pendings = 0;
for (User member: members) {
CheckResult result = member.checkApprovalSince(request.getBaseUpdate());
if (result == CheckResult.ACCEPT) {
Vote.Result result = member.checkVoteSince(request.getBaseUpdate());
if (result == null) {
pendings++;
} else if (result.isAccept()) {
approvals++;
} else if (result == CheckResult.PENDING_BLOCK) {
pendingBlock = true;
pendings++;
} else if (result == CheckResult.PENDING) {
pendings++;
}
}
if (approvals >= getLeastApprovals()) {
return CheckResult.ACCEPT;
return accept("Get at least " + getLeastApprovals() + " approvals from team '" + getTeam().getName() + "'.");
} else if (getLeastApprovals() - approvals > pendings) {
return CheckResult.REJECT;
return reject("Can not get at least " + getLeastApprovals() + " approvals from team '" + getTeam().getName() + "'.");
} else {
for (int i=0; i<getLeastApprovals()-approvals; i++)
request.requestVote(members);
int lackApprovals = getLeastApprovals() - approvals;
request.inviteToVote(members, lackApprovals);
if (pendingBlock)
return CheckResult.PENDING_BLOCK;
else
return CheckResult.PENDING;
return pending("To be approved by " + lackApprovals + " user(s) from team '" + getTeam().getName() + "'.");
}
}

View File

@ -5,8 +5,9 @@ import com.pmease.commons.util.EasySet;
import com.pmease.gitop.core.manager.UserManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.User;
import com.pmease.gitop.core.model.Vote;
public class ApprovedBySpecifiedUser implements GateKeeper {
public class ApprovedBySpecifiedUser extends AbstractGateKeeper {
private Long userId;
@ -23,11 +24,15 @@ public class ApprovedBySpecifiedUser implements GateKeeper {
UserManager userManager = AppLoader.getInstance(UserManager.class);
User user = userManager.load(getUserId());
CheckResult result = user.checkApprovalSince(request.getBaseUpdate());
if (result.isPending())
request.requestVote(EasySet.of(user));
return result;
Vote.Result result = user.checkVoteSince(request.getBaseUpdate());
if (result == null) {
request.inviteToVote(EasySet.of(user), 1);
return pending("To be approved by user '" + user.getName() + "'.");
} else if (result.isAccept()) {
return accept("Approved by user '" + user.getName() + "'.");
} else {
return reject("Rejected by user '" + user.getName() + "'.");
}
}
@Override

View File

@ -0,0 +1,164 @@
package com.pmease.gitop.core.gatekeeper;
import java.util.List;
import com.pmease.commons.util.EasyList;
public abstract class CheckResult {
private final List<String> reasons;
public CheckResult(List<String> reasons) {
this.reasons = reasons;
}
public CheckResult(String reason) {
this.reasons = EasyList.of(reason);
}
public List<String> getReasons() {
return reasons;
}
public abstract boolean isAccept();
public abstract boolean isReject();
public abstract boolean isPending();
public abstract boolean isBlock();
/* accept the merge request. */
public static class Accept extends CheckResult {
public Accept(List<String> reasons) {
super(reasons);
}
public Accept(String reason) {
super(reason);
}
@Override
public boolean isPending() {
return false;
}
@Override
public boolean isAccept() {
return true;
}
@Override
public boolean isReject() {
return false;
}
@Override
public boolean isBlock() {
return false;
}
};
/* reject the merge request. */
public static class Reject extends CheckResult {
public Reject(List<String> reasons) {
super(reasons);
}
public Reject(String reason) {
super(reason);
}
@Override
public boolean isAccept() {
return false;
}
@Override
public boolean isReject() {
return true;
}
@Override
public boolean isPending() {
return false;
}
@Override
public boolean isBlock() {
return false;
}
};
/* merge request acceptance check is pending and result is unknown yet */
public static class Pending extends CheckResult {
public Pending(List<String> reasons) {
super(reasons);
}
public Pending(String reason) {
super(reason);
}
@Override
public boolean isAccept() {
return false;
}
@Override
public boolean isReject() {
return false;
}
@Override
public boolean isPending() {
return true;
}
@Override
public boolean isBlock() {
return false;
}
};
/*
* same as Pending, but followed gate keeper should not be checked unless result
* of this gate keeper has been determined.
*/
public static class Block extends CheckResult {
public Block(List<String> reasons) {
super(reasons);
}
public Block(String reason) {
super(reason);
}
@Override
public boolean isPending() {
return false;
}
@Override
public boolean isAccept() {
return false;
}
@Override
public boolean isReject() {
return false;
}
@Override
public boolean isBlock() {
return true;
}
};
}

View File

@ -5,45 +5,5 @@ import com.pmease.gitop.core.model.MergeRequest;
public interface GateKeeper extends Trimmable {
public enum CheckResult {
/* accept the merge request. */
ACCEPT {
@Override
public boolean isPending() {
return false;
}
},
/* reject the merge request. */
REJECT {
@Override
public boolean isPending() {
return false;
}
},
/* merge request acceptance check is pending and result is unknown yet */
PENDING {
@Override
public boolean isPending() {
return true;
}
},
/*
* same as PENDING, but followed gate keeper should not be checked unless result
* of this gate keeper has been determined.
*/
PENDING_BLOCK {
@Override
public boolean isPending() {
return true;
}
};
public abstract boolean isPending();
};
CheckResult check(MergeRequest request);
}

View File

@ -5,12 +5,10 @@ import java.util.HashSet;
import javax.validation.constraints.Min;
import com.pmease.gitop.core.Gitop;
import com.pmease.gitop.core.manager.TeamManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.Team;
import com.pmease.gitop.core.model.TeamMembership;
import com.pmease.gitop.core.model.User;
import com.pmease.gitop.core.model.Vote;
public class GetMinScoreFromSpecifiedTeam extends TeamAwareGateKeeper {
@ -37,56 +35,46 @@ public class GetMinScoreFromSpecifiedTeam extends TeamAwareGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
TeamManager teamManager = Gitop.getInstance(TeamManager.class);
Team team = teamManager.load(getTeamId());
Collection<User> members = new HashSet<User>();
for (TeamMembership membership: team.getMemberships())
for (TeamMembership membership: getTeam().getMemberships())
members.add(membership.getUser());
boolean pendingBlock = false;
int score = 0;
int pending = 0;
int pendings = 0;
for (User member: members) {
CheckResult result = member.checkApprovalSince(request.getBaseUpdate());
if (result == CheckResult.ACCEPT) {
Vote.Result result = member.checkVoteSince(request.getBaseUpdate());
if (result == null) {
pendings++;
} else if (result.isAccept()) {
score++;
} else if (result == CheckResult.REJECT) {
score--;
} else if (result == CheckResult.PENDING_BLOCK) {
pendingBlock = true;
pending++;
} else {
pending++;
score--;
}
}
if (score + pending < getMinScore()) {
return CheckResult.REJECT;
if (score + pendings < getMinScore()) {
return reject("Can not get min score " + getMinScore() + " from team '" + getTeam().getName() + "'.");
} else {
int lackApprovals;
if (isRequireVoteOfAllMembers()) {
if (score - pending >= getMinScore())
return CheckResult.ACCEPT;
int temp = getMinScore() + pending - score;
if (score - pendings >= getMinScore())
return accept("Get min score " + getMinScore() + " from team '" + getTeam().getName() + "'.");
int temp = getMinScore() + pendings - score;
lackApprovals = temp / 2;
if (temp % 2 != 0)
lackApprovals++;
if (lackApprovals > pending)
lackApprovals = pending;
if (lackApprovals > pendings)
lackApprovals = pendings;
} else {
if (score >= getMinScore())
return CheckResult.ACCEPT;
return accept("Get min score " + getMinScore() + " from team '" + getTeam().getName() + "'.");
lackApprovals = getMinScore() - score;
}
for (int i=0; i<lackApprovals; i++)
request.requestVote(members);
request.inviteToVote(members, lackApprovals);
if (pendingBlock)
return CheckResult.PENDING_BLOCK;
else
return CheckResult.PENDING;
return pending("To be approved by " + lackApprovals + " users from team '" + getTeam().getName() + ".");
}
}

View File

@ -7,9 +7,9 @@ public class HasSourceBranch extends AbstractGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
if (request.getSource() != null)
return CheckResult.ACCEPT;
return accept("Associated with source branch.");
else
return CheckResult.REJECT;
return reject("Not associated with source branch.");
}
}

View File

@ -31,18 +31,18 @@ public class IfThenGateKeeper extends AbstractGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
CheckResult ifResult = getIfGate().check(request);
if (ifResult == CheckResult.ACCEPT) {
if (ifResult.isAccept()) {
return getThenGate().check(request);
} else if (ifResult == CheckResult.REJECT) {
return CheckResult.ACCEPT;
} else if (ifResult == CheckResult.PENDING_BLOCK) {
return CheckResult.PENDING_BLOCK;
} else if (ifResult.isReject()) {
return accept(ifResult.getReasons());
} else if (ifResult.isBlock()) {
return ifResult;
} else {
CheckResult thenResult = getThenGate().check(request);
if (thenResult == CheckResult.ACCEPT)
return CheckResult.ACCEPT;
if (thenResult.isAccept())
return thenResult;
else
return CheckResult.PENDING;
return ifResult;
}
}

View File

@ -7,9 +7,9 @@ public class IsFastForward extends AbstractGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
if (request.isFastForward())
return CheckResult.ACCEPT;
return accept("Is fast-forward.");
else
return CheckResult.REJECT;
return reject("None fast-forward.");
}
}

View File

@ -6,7 +6,7 @@ public class NeverAccept extends AbstractGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
return CheckResult.REJECT;
return reject("never");
}
}

View File

@ -1,26 +1,22 @@
package com.pmease.gitop.core.gatekeeper;
import com.pmease.commons.loader.AppLoader;
import com.pmease.gitop.core.manager.TeamManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.Team;
import com.pmease.gitop.core.model.TeamMembership;
import com.pmease.gitop.core.model.Vote;
public class NoRejectionBySpecifiedTeam extends TeamAwareGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
TeamManager teamManager = AppLoader.getInstance(TeamManager.class);
Team team = teamManager.load(getTeamId());
for (TeamMembership membership: team.getMemberships()) {
CheckResult result = membership.getUser().checkApprovalSince(request.getBaseUpdate());
if (result == CheckResult.REJECT) {
return CheckResult.REJECT;
for (TeamMembership membership: getTeam().getMemberships()) {
Vote.Result result = membership.getUser().checkVoteSince(request.getBaseUpdate());
if (result.isReject()) {
return reject("Rejected by user '" + membership.getUser().getName() + "'.");
}
}
return CheckResult.ACCEPT;
return accept("Not rejected by anyone from team '" + getTeam().getName() + ".");
}
}

View File

@ -2,7 +2,7 @@ package com.pmease.gitop.core.gatekeeper;
import com.pmease.gitop.core.model.MergeRequest;
public class NotGateKeeper implements GateKeeper {
public class NotGateKeeper extends AbstractGateKeeper {
private GateKeeper gateKeeper;
@ -18,12 +18,12 @@ public class NotGateKeeper implements GateKeeper {
public CheckResult check(MergeRequest request) {
CheckResult result = getGateKeeper().check(request);
if (result == CheckResult.PENDING)
return result;
else if (result == CheckResult.ACCEPT)
return CheckResult.REJECT;
if (result.isAccept())
return reject(result.getReasons());
else if (result.isReject())
return accept(result.getReasons());
else
return CheckResult.ACCEPT;
return result;
}
@Override

View File

@ -8,7 +8,7 @@ import com.pmease.commons.util.trimmable.TrimUtils;
import com.pmease.commons.util.trimmable.Trimmable;
import com.pmease.gitop.core.model.MergeRequest;
public class OrGateKeeper implements GateKeeper {
public class OrGateKeeper extends AbstractGateKeeper {
private List<GateKeeper> gateKeepers = new ArrayList<GateKeeper>();
@ -22,20 +22,27 @@ public class OrGateKeeper implements GateKeeper {
@Override
public CheckResult check(MergeRequest request) {
boolean pending = false;
List<String> pendingReasons = new ArrayList<String>();
List<String> rejectReasons = new ArrayList<String>();
for (GateKeeper each: getGateKeepers()) {
CheckResult result = each.check(request);
if (result == CheckResult.ACCEPT || result == CheckResult.PENDING_BLOCK)
if (result.isReject()) {
rejectReasons.addAll(result.getReasons());
} else if (result.isAccept()) {
return result;
else if (result == CheckResult.PENDING)
pending = true;
} else if (result.isBlock()) {
result.getReasons().addAll(pendingReasons);
return result;
} else {
pendingReasons.addAll(result.getReasons());
}
}
if (pending)
return CheckResult.PENDING;
if (!pendingReasons.isEmpty())
return pending(pendingReasons);
else
return CheckResult.REJECT;
return reject(rejectReasons);
}
@Override

View File

@ -11,7 +11,7 @@ import com.pmease.gitop.core.manager.BranchManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.Repository;
public class SubmittedToSpecifiedBranch implements GateKeeper {
public class SubmittedToSpecifiedBranch extends AbstractGateKeeper {
private String branchPatterns;
@ -31,10 +31,12 @@ public class SubmittedToSpecifiedBranch implements GateKeeper {
EntityMatcher entityMatcher = new EntityMatcher(entityLoader, new WildcardPathMatcher());
PatternSetMatcher patternSetMatcher = new PatternSetMatcher(entityMatcher);
EntityPatternSet patternSet = EntityPatternSet.fromStored(getBranchPatterns(), entityLoader);
if (patternSetMatcher.matches(getBranchPatterns(), request.getDestination().getName()))
return CheckResult.ACCEPT;
return accept("Target branch matches pattern '" + patternSet + "'.");
else
return CheckResult.REJECT;
return reject("Target branch does not match pattern '" + patternSet + "'.");
}
@Override

View File

@ -7,9 +7,9 @@ public class SubmittedViaPush extends AbstractGateKeeper {
@Override
public CheckResult check(MergeRequest request) {
if (request.isAutoCreated())
return CheckResult.ACCEPT;
return accept("Submitted via push.");
else
return CheckResult.REJECT;
return reject("Not submitted via push.");
}
}

View File

@ -2,6 +2,7 @@ package com.pmease.gitop.core.gatekeeper;
import com.pmease.gitop.core.Gitop;
import com.pmease.gitop.core.manager.TeamManager;
import com.pmease.gitop.core.model.Team;
public abstract class TeamAwareGateKeeper extends AbstractGateKeeper {
@ -14,6 +15,10 @@ public abstract class TeamAwareGateKeeper extends AbstractGateKeeper {
public void setTeamId(Long teamId) {
this.teamId = teamId;
}
public Team getTeam() {
return Gitop.getInstance(TeamManager.class).load(getTeamId());
}
@Override
public Object trim(Object context) {

View File

@ -46,12 +46,12 @@ public class TouchSpecifiedFiles extends AbstractGateKeeper {
for (String file: touchedFiles) {
if (WildcardUtils.matchPath(getFilePaths(), file)) {
request.setBaseUpdate(update);
return CheckResult.ACCEPT;
return accept("Touched files match pattern '" + getFilePaths() + "'.");
}
}
}
return CheckResult.REJECT;
return reject("No touched files match pattern '" + getFilePaths() + "'.");
}
}

View File

@ -1,15 +0,0 @@
package com.pmease.gitop.core.manager;
import com.google.inject.ImplementedBy;
import com.pmease.commons.persistence.dao.GenericDao;
import com.pmease.gitop.core.manager.impl.DefaultPendingVoteManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.PendingVote;
import com.pmease.gitop.core.model.User;
@ImplementedBy(DefaultPendingVoteManager.class)
public interface PendingVoteManager extends GenericDao<PendingVote> {
PendingVote find(User reviewer, MergeRequest request);
}

View File

@ -0,0 +1,15 @@
package com.pmease.gitop.core.manager;
import com.google.inject.ImplementedBy;
import com.pmease.commons.persistence.dao.GenericDao;
import com.pmease.gitop.core.manager.impl.DefaultVoteInvitationManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.VoteInvitation;
import com.pmease.gitop.core.model.User;
@ImplementedBy(DefaultVoteInvitationManager.class)
public interface VoteInvitationManager extends GenericDao<VoteInvitation> {
VoteInvitation find(User reviewer, MergeRequest request);
}

View File

@ -1,41 +0,0 @@
package com.pmease.gitop.core.manager.impl;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Restrictions;
import com.pmease.commons.persistence.Transactional;
import com.pmease.commons.persistence.dao.DefaultGenericDao;
import com.pmease.commons.persistence.dao.GeneralDao;
import com.pmease.gitop.core.manager.PendingVoteManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.PendingVote;
import com.pmease.gitop.core.model.User;
@Singleton
public class DefaultPendingVoteManager extends DefaultGenericDao<PendingVote> implements PendingVoteManager {
public DefaultPendingVoteManager(GeneralDao generalDao, Provider<Session> sessionProvider) {
super(generalDao, sessionProvider);
}
@Transactional
@Override
public PendingVote find(User reviewer, MergeRequest request) {
return find(new Criterion[]{Restrictions.eq("reviewer", reviewer), Restrictions.eq("request", request)});
}
@Transactional
@Override
public void save(PendingVote pendingVote) {
if (pendingVote.getId() == null) {
pendingVote.getRequest().getPendingVotes().add(pendingVote);
pendingVote.getReviewer().getPendingVotes().add(pendingVote);
}
super.save(pendingVote);
}
}

View File

@ -0,0 +1,49 @@
package com.pmease.gitop.core.manager.impl;
import javax.inject.Provider;
import javax.inject.Singleton;
import org.hibernate.Session;
import org.hibernate.criterion.Criterion;
import org.hibernate.criterion.Restrictions;
import com.pmease.commons.persistence.Transactional;
import com.pmease.commons.persistence.dao.DefaultGenericDao;
import com.pmease.commons.persistence.dao.GeneralDao;
import com.pmease.gitop.core.manager.VoteInvitationManager;
import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.VoteInvitation;
import com.pmease.gitop.core.model.User;
@Singleton
public class DefaultVoteInvitationManager extends DefaultGenericDao<VoteInvitation> implements VoteInvitationManager {
public DefaultVoteInvitationManager(GeneralDao generalDao, Provider<Session> sessionProvider) {
super(generalDao, sessionProvider);
}
@Transactional
@Override
public VoteInvitation find(User reviewer, MergeRequest request) {
return find(new Criterion[]{Restrictions.eq("reviewer", reviewer), Restrictions.eq("request", request)});
}
@Transactional
@Override
public void save(VoteInvitation voteInvitation) {
if (voteInvitation.getId() == null) {
voteInvitation.getRequest().getVoteInvitations().add(voteInvitation);
voteInvitation.getReviewer().getVoteInvitations().add(voteInvitation);
}
super.save(voteInvitation);
}
@Transactional
@Override
public void delete(VoteInvitation voteInvitation) {
voteInvitation.getRequest().getVoteInvitations().remove(voteInvitation);
voteInvitation.getReviewer().getVoteInvitations().remove(voteInvitation);
super.delete(voteInvitation);
}
}

View File

@ -19,6 +19,7 @@ import javax.persistence.OneToMany;
import org.hibernate.annotations.FetchMode;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.pmease.commons.git.CalcMergeBaseCommand;
import com.pmease.commons.git.CheckAncestorCommand;
@ -26,7 +27,9 @@ import com.pmease.commons.git.FindChangedFilesCommand;
import com.pmease.commons.git.Git;
import com.pmease.commons.persistence.AbstractEntity;
import com.pmease.gitop.core.Gitop;
import com.pmease.gitop.core.manager.PendingVoteManager;
import com.pmease.gitop.core.gatekeeper.CheckResult;
import com.pmease.gitop.core.gatekeeper.GateKeeper;
import com.pmease.gitop.core.manager.VoteInvitationManager;
import com.pmease.gitop.core.manager.RepositoryManager;
@SuppressWarnings("serial")
@ -54,6 +57,8 @@ public class MergeRequest extends AbstractEntity {
@Column(nullable=false)
private Status status = Status.OPEN;
private transient Optional<CheckResult> checkResult;
private transient Boolean merged;
@ -64,12 +69,14 @@ public class MergeRequest extends AbstractEntity {
private transient List<MergeRequestUpdate> effectiveUpdates;
private transient MergeRequestUpdate baseUpdate;
private transient Collection<User> potentialVoters;
@OneToMany(mappedBy="request")
private Collection<MergeRequestUpdate> updates = new ArrayList<MergeRequestUpdate>();
@OneToMany(mappedBy="request")
private Collection<PendingVote> pendingVotes = new ArrayList<PendingVote>();
private Collection<VoteInvitation> voteInvitations = new ArrayList<VoteInvitation>();
public String getTitle() {
return title;
@ -115,12 +122,12 @@ public class MergeRequest extends AbstractEntity {
this.updates = updates;
}
public Collection<PendingVote> getPendingVotes() {
return pendingVotes;
public Collection<VoteInvitation> getVoteInvitations() {
return voteInvitations;
}
public void setPendingVotes(Collection<PendingVote> pendingVotes) {
this.pendingVotes = pendingVotes;
public void setVoteInvitations(Collection<VoteInvitation> voteInvitations) {
this.voteInvitations = voteInvitations;
}
public Status getStatus() {
@ -238,15 +245,61 @@ public class MergeRequest extends AbstractEntity {
}
return mergeBase;
}
public void requestVote(Collection<User> candidates) {
Set<User> pendingUsers = new HashSet<User>();
for (PendingVote each: getPendingVotes())
pendingUsers.add(each.getReviewer());
pendingUsers.retainAll(candidates);
/**
* Find potential voters for this request. Potential voters will be
* presented with the vote/reject button when they review the request.
* However they may not receive vote invitation.
* <p>
* @return
* a collection of potential voters
*/
public Collection<User> findPotentialVoters() {
check(false);
return getPotentialVoters();
}
private Collection<User> getPotentialVoters() {
if (potentialVoters == null)
potentialVoters = new HashSet<User>();
return potentialVoters;
}
/**
* Invite specified number of users to vote for this request.
* <p>
* @param users
* a collection of users to invite users from
* @param count
* number of users to invite
*/
public void inviteToVote(final Collection<User> users, int count) {
Collection<User> candidates = new HashSet<User>(users);
// submitter is not allowed to vote for this request
candidates.remove(getSubmitter());
if (pendingUsers.isEmpty()) {
// users already voted for latest update should be excluded
for (Vote vote: getLatestUpdate().getVotes())
candidates.remove(vote.getReviewer());
getPotentialVoters().addAll(candidates);
/*
* users already voted since base update should be excluded from
* invitation list as their votes are still valid
*/
for (Vote vote: getBaseUpdate().listVotesOnwards()) {
candidates.remove(vote.getReviewer());
}
Set<User> invited = new HashSet<User>();
for (VoteInvitation each: getVoteInvitations())
invited.add(each.getReviewer());
invited.retainAll(candidates);
for (int i=0; i<count-invited.size(); i++) {
User selected = Collections.min(candidates, new Comparator<User>() {
@Override
@ -255,14 +308,49 @@ public class MergeRequest extends AbstractEntity {
}
});
candidates.remove(selected);
PendingVote pendingVote = new PendingVote();
pendingVote.setRequest(this);
pendingVote.setReviewer(selected);
Gitop.getInstance(PendingVoteManager.class).save(pendingVote);
VoteInvitation invitation = new VoteInvitation();
invitation.setRequest(this);
invitation.setReviewer(selected);
Gitop.getInstance(VoteInvitationManager.class).save(invitation);
}
}
/**
* Check this request with gate keeper of target repository.
* <p>
* @param force
* whether or not to force the check. Since the check might be time-consuming, Gitop
* caches the check result, and returns the cached result for subsequent checks if
* force flag is not set.
* @return
* check result of gate keeper, or Optional.absent() if no gate keeper is defined
*/
public Optional<CheckResult> check(boolean force) {
if (checkResult == null || force) {
GateKeeper gateKeeper = getDestination().getRepository().getGateKeeper();
if (gateKeeper != null) {
checkResult = Optional.of(gateKeeper.check(this));
Collection<VoteInvitation> withdraws = new HashSet<VoteInvitation>();
for (VoteInvitation invitation: getVoteInvitations()) {
if (!getPotentialVoters().contains(invitation.getReviewer())) {
withdraws.add(invitation);
}
}
for (VoteInvitation invitation: withdraws) {
Gitop.getInstance(VoteInvitationManager.class).delete(invitation);
}
} else {
checkResult = Optional.absent();
}
}
return checkResult;
}
}

View File

@ -34,7 +34,7 @@ public class MergeRequestUpdate extends AbstractEntity {
@OneToMany(mappedBy="update")
private Collection<Vote> votes = new ArrayList<Vote>();
public MergeRequest getRequest() {
return request;
}

View File

@ -72,6 +72,8 @@ public class Repository extends AbstractEntity implements UserBelonging {
}
public GateKeeper getGateKeeper() {
if (gateKeeper != null)
gateKeeper = (GateKeeper) gateKeeper.trim(this);
return gateKeeper;
}

View File

@ -7,7 +7,6 @@ import javax.persistence.Entity;
import javax.persistence.OneToMany;
import com.pmease.commons.security.AbstractUser;
import com.pmease.gitop.core.gatekeeper.GateKeeper.CheckResult;
import com.pmease.gitop.core.permission.object.ProtectedObject;
import com.pmease.gitop.core.permission.object.UserBelonging;
@ -48,7 +47,7 @@ public class User extends AbstractUser implements ProtectedObject {
private Collection<Vote> votes = new ArrayList<Vote>();
@OneToMany(mappedBy="reviewer")
private Collection<PendingVote> pendingVotes = new ArrayList<PendingVote>();
private Collection<VoteInvitation> voteVitations = new ArrayList<VoteInvitation>();
public String getDescription() {
return description;
@ -106,41 +105,38 @@ public class User extends AbstractUser implements ProtectedObject {
this.votes = votes;
}
public Collection<PendingVote> getPendingVotes() {
return pendingVotes;
public Collection<VoteInvitation> getVoteInvitations() {
return voteVitations;
}
public void setPendingVotes(Collection<PendingVote> pendingVotes) {
this.pendingVotes = pendingVotes;
public void setVoteInvitations(Collection<VoteInvitation> voteInvitations) {
this.voteVitations = voteInvitations;
}
@Override
public boolean has(ProtectedObject object) {
if (object instanceof User) {
User user = (User) object;
return user.getId().equals(getId());
return user.equals(this);
} else if (object instanceof UserBelonging) {
UserBelonging userBelonging = (UserBelonging) object;
return userBelonging.getUser().getId().equals(getId());
return userBelonging.getUser().equals(this);
} else {
return false;
}
}
public CheckResult checkApprovalSince(MergeRequestUpdate update) {
if (update.getRequest().getSubmitter().getId().equals(getId()))
return CheckResult.ACCEPT;
public Vote.Result checkVoteSince(MergeRequestUpdate update) {
if (update.getRequest().getSubmitter().equals(this))
return Vote.Result.ACCEPT;
for (Vote vote: update.listVotesOnwards()) {
if (vote.getReviewer().equals(this)) {
if (vote.getResult() == Vote.Result.ACCEPT)
return CheckResult.ACCEPT;
else
return CheckResult.REJECT;
return vote.getResult();
}
}
return CheckResult.PENDING;
return null;
}
}

View File

@ -19,7 +19,34 @@ import com.pmease.commons.persistence.AbstractEntity;
})
public class Vote extends AbstractEntity {
public static enum Result {ACCEPT, REJECT};
public static enum Result {
ACCEPT {
@Override
public boolean isAccept() {
return true;
}
@Override
public boolean isReject() {
return false;
}
},
REJECT {
@Override
public boolean isAccept() {
return false;
}
@Override
public boolean isReject() {
return true;
}
};
public abstract boolean isAccept();
public abstract boolean isReject();
};
@ManyToOne(fetch=FetchType.EAGER)
@org.hibernate.annotations.Fetch(FetchMode.SELECT)

View File

@ -16,7 +16,7 @@ import com.pmease.commons.persistence.AbstractEntity;
@Table(uniqueConstraints={
@UniqueConstraint(columnNames={"reviewer", "request"})
})
public class PendingVote extends AbstractEntity {
public class VoteInvitation extends AbstractEntity {
@ManyToOne(fetch=FetchType.EAGER)
@org.hibernate.annotations.Fetch(FetchMode.SELECT)