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; return input;
} }
@Override
public String toString() {
return new PatternSet(asInput()).toString();
}
public static EntityPatternSet fromInput(List<ExclusiveAwarePattern> input, EntityLoader entityLoader) { public static EntityPatternSet fromInput(List<ExclusiveAwarePattern> input, EntityLoader entityLoader) {
List<ExclusiveAwarePattern> stored = new ArrayList<ExclusiveAwarePattern>(); List<ExclusiveAwarePattern> stored = new ArrayList<ExclusiveAwarePattern>();
for (ExclusiveAwarePattern eachInput: input) { 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.loader.AbstractPlugin;
import com.pmease.commons.persistence.AbstractEntity; import com.pmease.commons.persistence.AbstractEntity;
import com.pmease.commons.persistence.extensionpoints.ModelContribution; 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.model.User;
import com.pmease.gitop.core.web.asset.AssetLocator; import com.pmease.gitop.core.web.asset.AssetLocator;
@ -42,7 +43,7 @@ public class CorePlugin extends AbstractPlugin {
public Collection<Class<? extends AbstractEntity>> getModelClasses() { public Collection<Class<? extends AbstractEntity>> getModelClasses() {
Collection<Class<? extends AbstractEntity>> modelClasses = Collection<Class<? extends AbstractEntity>> modelClasses =
new HashSet<Class<? extends AbstractEntity>>(); new HashSet<Class<? extends AbstractEntity>>();
modelClasses.add(User.class); modelClasses.addAll(ClassUtils.findSubClasses(AbstractEntity.class, User.class));
return modelClasses; return modelClasses;
} }
}); });

View File

@ -1,5 +1,7 @@
package com.pmease.gitop.core.gatekeeper; package com.pmease.gitop.core.gatekeeper;
import java.util.List;
import javax.annotation.Nullable; import javax.annotation.Nullable;
public abstract class AbstractGateKeeper implements GateKeeper { public abstract class AbstractGateKeeper implements GateKeeper {
@ -9,4 +11,36 @@ public abstract class AbstractGateKeeper implements GateKeeper {
return this; 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 @Override
public CheckResult check(MergeRequest request) { 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.commons.util.trimmable.TrimUtils;
import com.pmease.gitop.core.model.MergeRequest; import com.pmease.gitop.core.model.MergeRequest;
public class AndGateKeeper implements GateKeeper { public class AndGateKeeper extends AbstractGateKeeper {
private List<GateKeeper> gateKeepers = new ArrayList<GateKeeper>(); private List<GateKeeper> gateKeepers = new ArrayList<GateKeeper>();
@ -21,20 +21,27 @@ public class AndGateKeeper implements GateKeeper {
@Override @Override
public CheckResult check(MergeRequest request) { public CheckResult check(MergeRequest request) {
boolean pending = false; List<String> pendingReasons = new ArrayList<String>();
List<String> acceptReasons = new ArrayList<String>();
for (GateKeeper each: getGateKeepers()) { for (GateKeeper each: getGateKeepers()) {
CheckResult result = each.check(request); 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; return result;
else if (result == CheckResult.PENDING) } else if (result.isBlock()) {
pending = true; result.getReasons().addAll(pendingReasons);
return result;
} else {
pendingReasons.addAll(result.getReasons());
}
} }
if (pending) if (!pendingReasons.isEmpty())
return CheckResult.PENDING; return pending(pendingReasons);
else else
return CheckResult.ACCEPT; return accept(acceptReasons);
} }
@Override @Override

View File

@ -20,8 +20,14 @@ public class ApprovedByAuthorizedUsers extends AbstractGateKeeper {
CheckResult result = or.check(request); CheckResult result = or.check(request);
if (result.isPending()) { if (result.isAccept()) {
request.requestVote(authorizedUsers); 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; return result;
} }

View File

@ -11,7 +11,14 @@ public class ApprovedByMajoritiesOfSpecifiedTeam extends TeamAwareGateKeeper {
gateKeeper.setRequireVoteOfAllMembers(true); gateKeeper.setRequireVoteOfAllMembers(true);
gateKeeper.setTeamId(getTeamId()); 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.commons.util.EasySet;
import com.pmease.gitop.core.model.MergeRequest; import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.User; import com.pmease.gitop.core.model.User;
import com.pmease.gitop.core.model.Vote;
public class ApprovedByRepositoryOwner extends AbstractGateKeeper { public class ApprovedByRepositoryOwner extends AbstractGateKeeper {
@ -10,10 +11,16 @@ public class ApprovedByRepositoryOwner extends AbstractGateKeeper {
public CheckResult check(MergeRequest request) { public CheckResult check(MergeRequest request) {
User repositoryOwner = request.getDestination().getRepository().getOwner(); User repositoryOwner = request.getDestination().getRepository().getOwner();
CheckResult result = repositoryOwner.checkApprovalSince(request.getBaseUpdate()); Vote.Result result = repositoryOwner.checkVoteSince(request.getBaseUpdate());
if (result.isPending())
request.requestVote(EasySet.of(repositoryOwner)); if (result == null) {
return result; 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 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.MergeRequest;
import com.pmease.gitop.core.model.Team;
import com.pmease.gitop.core.model.TeamMembership; import com.pmease.gitop.core.model.TeamMembership;
import com.pmease.gitop.core.model.User; import com.pmease.gitop.core.model.User;
import com.pmease.gitop.core.model.Vote;
public class ApprovedBySpecifiedTeam extends TeamAwareGateKeeper { public class ApprovedBySpecifiedTeam extends TeamAwareGateKeeper {
@ -27,41 +25,31 @@ public class ApprovedBySpecifiedTeam extends TeamAwareGateKeeper {
@Override @Override
public CheckResult check(MergeRequest request) { public CheckResult check(MergeRequest request) {
TeamManager teamManager = AppLoader.getInstance(TeamManager.class);
Team team = teamManager.load(getTeamId());
Collection<User> members = new HashSet<User>(); Collection<User> members = new HashSet<User>();
for (TeamMembership membership: team.getMemberships()) for (TeamMembership membership: getTeam().getMemberships())
members.add(membership.getUser()); members.add(membership.getUser());
boolean pendingBlock = false;
int approvals = 0; int approvals = 0;
int pendings = 0; int pendings = 0;
for (User member: members) { for (User member: members) {
CheckResult result = member.checkApprovalSince(request.getBaseUpdate()); Vote.Result result = member.checkVoteSince(request.getBaseUpdate());
if (result == CheckResult.ACCEPT) { if (result == null) {
pendings++;
} else if (result.isAccept()) {
approvals++; approvals++;
} else if (result == CheckResult.PENDING_BLOCK) {
pendingBlock = true;
pendings++;
} else if (result == CheckResult.PENDING) {
pendings++;
} }
} }
if (approvals >= getLeastApprovals()) { if (approvals >= getLeastApprovals()) {
return CheckResult.ACCEPT; return accept("Get at least " + getLeastApprovals() + " approvals from team '" + getTeam().getName() + "'.");
} else if (getLeastApprovals() - approvals > pendings) { } else if (getLeastApprovals() - approvals > pendings) {
return CheckResult.REJECT; return reject("Can not get at least " + getLeastApprovals() + " approvals from team '" + getTeam().getName() + "'.");
} else { } else {
for (int i=0; i<getLeastApprovals()-approvals; i++) int lackApprovals = getLeastApprovals() - approvals;
request.requestVote(members);
if (pendingBlock) request.inviteToVote(members, lackApprovals);
return CheckResult.PENDING_BLOCK;
else return pending("To be approved by " + lackApprovals + " user(s) from team '" + getTeam().getName() + "'.");
return CheckResult.PENDING;
} }
} }

View File

@ -5,8 +5,9 @@ import com.pmease.commons.util.EasySet;
import com.pmease.gitop.core.manager.UserManager; import com.pmease.gitop.core.manager.UserManager;
import com.pmease.gitop.core.model.MergeRequest; import com.pmease.gitop.core.model.MergeRequest;
import com.pmease.gitop.core.model.User; 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; private Long userId;
@ -23,11 +24,15 @@ public class ApprovedBySpecifiedUser implements GateKeeper {
UserManager userManager = AppLoader.getInstance(UserManager.class); UserManager userManager = AppLoader.getInstance(UserManager.class);
User user = userManager.load(getUserId()); User user = userManager.load(getUserId());
CheckResult result = user.checkApprovalSince(request.getBaseUpdate()); Vote.Result result = user.checkVoteSince(request.getBaseUpdate());
if (result.isPending()) if (result == null) {
request.requestVote(EasySet.of(user)); request.inviteToVote(EasySet.of(user), 1);
return result; 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 @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 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); CheckResult check(MergeRequest request);
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,22 @@
package com.pmease.gitop.core.gatekeeper; 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.MergeRequest;
import com.pmease.gitop.core.model.Team;
import com.pmease.gitop.core.model.TeamMembership; import com.pmease.gitop.core.model.TeamMembership;
import com.pmease.gitop.core.model.Vote;
public class NoRejectionBySpecifiedTeam extends TeamAwareGateKeeper { public class NoRejectionBySpecifiedTeam extends TeamAwareGateKeeper {
@Override @Override
public CheckResult check(MergeRequest request) { public CheckResult check(MergeRequest request) {
TeamManager teamManager = AppLoader.getInstance(TeamManager.class);
Team team = teamManager.load(getTeamId());
for (TeamMembership membership: team.getMemberships()) { for (TeamMembership membership: getTeam().getMemberships()) {
CheckResult result = membership.getUser().checkApprovalSince(request.getBaseUpdate()); Vote.Result result = membership.getUser().checkVoteSince(request.getBaseUpdate());
if (result == CheckResult.REJECT) { if (result.isReject()) {
return CheckResult.REJECT; 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; import com.pmease.gitop.core.model.MergeRequest;
public class NotGateKeeper implements GateKeeper { public class NotGateKeeper extends AbstractGateKeeper {
private GateKeeper gateKeeper; private GateKeeper gateKeeper;
@ -18,12 +18,12 @@ public class NotGateKeeper implements GateKeeper {
public CheckResult check(MergeRequest request) { public CheckResult check(MergeRequest request) {
CheckResult result = getGateKeeper().check(request); CheckResult result = getGateKeeper().check(request);
if (result == CheckResult.PENDING) if (result.isAccept())
return result; return reject(result.getReasons());
else if (result == CheckResult.ACCEPT) else if (result.isReject())
return CheckResult.REJECT; return accept(result.getReasons());
else else
return CheckResult.ACCEPT; return result;
} }
@Override @Override

View File

@ -8,7 +8,7 @@ import com.pmease.commons.util.trimmable.TrimUtils;
import com.pmease.commons.util.trimmable.Trimmable; import com.pmease.commons.util.trimmable.Trimmable;
import com.pmease.gitop.core.model.MergeRequest; import com.pmease.gitop.core.model.MergeRequest;
public class OrGateKeeper implements GateKeeper { public class OrGateKeeper extends AbstractGateKeeper {
private List<GateKeeper> gateKeepers = new ArrayList<GateKeeper>(); private List<GateKeeper> gateKeepers = new ArrayList<GateKeeper>();
@ -22,20 +22,27 @@ public class OrGateKeeper implements GateKeeper {
@Override @Override
public CheckResult check(MergeRequest request) { public CheckResult check(MergeRequest request) {
boolean pending = false; List<String> pendingReasons = new ArrayList<String>();
List<String> rejectReasons = new ArrayList<String>();
for (GateKeeper each: getGateKeepers()) { for (GateKeeper each: getGateKeepers()) {
CheckResult result = each.check(request); 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; return result;
else if (result == CheckResult.PENDING) } else if (result.isBlock()) {
pending = true; result.getReasons().addAll(pendingReasons);
return result;
} else {
pendingReasons.addAll(result.getReasons());
}
} }
if (pending) if (!pendingReasons.isEmpty())
return CheckResult.PENDING; return pending(pendingReasons);
else else
return CheckResult.REJECT; return reject(rejectReasons);
} }
@Override @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.MergeRequest;
import com.pmease.gitop.core.model.Repository; import com.pmease.gitop.core.model.Repository;
public class SubmittedToSpecifiedBranch implements GateKeeper { public class SubmittedToSpecifiedBranch extends AbstractGateKeeper {
private String branchPatterns; private String branchPatterns;
@ -31,10 +31,12 @@ public class SubmittedToSpecifiedBranch implements GateKeeper {
EntityMatcher entityMatcher = new EntityMatcher(entityLoader, new WildcardPathMatcher()); EntityMatcher entityMatcher = new EntityMatcher(entityLoader, new WildcardPathMatcher());
PatternSetMatcher patternSetMatcher = new PatternSetMatcher(entityMatcher); PatternSetMatcher patternSetMatcher = new PatternSetMatcher(entityMatcher);
EntityPatternSet patternSet = EntityPatternSet.fromStored(getBranchPatterns(), entityLoader);
if (patternSetMatcher.matches(getBranchPatterns(), request.getDestination().getName())) if (patternSetMatcher.matches(getBranchPatterns(), request.getDestination().getName()))
return CheckResult.ACCEPT; return accept("Target branch matches pattern '" + patternSet + "'.");
else else
return CheckResult.REJECT; return reject("Target branch does not match pattern '" + patternSet + "'.");
} }
@Override @Override

View File

@ -7,9 +7,9 @@ public class SubmittedViaPush extends AbstractGateKeeper {
@Override @Override
public CheckResult check(MergeRequest request) { public CheckResult check(MergeRequest request) {
if (request.isAutoCreated()) if (request.isAutoCreated())
return CheckResult.ACCEPT; return accept("Submitted via push.");
else 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.Gitop;
import com.pmease.gitop.core.manager.TeamManager; import com.pmease.gitop.core.manager.TeamManager;
import com.pmease.gitop.core.model.Team;
public abstract class TeamAwareGateKeeper extends AbstractGateKeeper { public abstract class TeamAwareGateKeeper extends AbstractGateKeeper {
@ -15,6 +16,10 @@ public abstract class TeamAwareGateKeeper extends AbstractGateKeeper {
this.teamId = teamId; this.teamId = teamId;
} }
public Team getTeam() {
return Gitop.getInstance(TeamManager.class).load(getTeamId());
}
@Override @Override
public Object trim(Object context) { public Object trim(Object context) {
TeamManager teamManager = Gitop.getInstance(TeamManager.class); TeamManager teamManager = Gitop.getInstance(TeamManager.class);

View File

@ -46,12 +46,12 @@ public class TouchSpecifiedFiles extends AbstractGateKeeper {
for (String file: touchedFiles) { for (String file: touchedFiles) {
if (WildcardUtils.matchPath(getFilePaths(), file)) { if (WildcardUtils.matchPath(getFilePaths(), file)) {
request.setBaseUpdate(update); 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 org.hibernate.annotations.FetchMode;
import com.google.common.base.Optional;
import com.google.common.base.Preconditions; import com.google.common.base.Preconditions;
import com.pmease.commons.git.CalcMergeBaseCommand; import com.pmease.commons.git.CalcMergeBaseCommand;
import com.pmease.commons.git.CheckAncestorCommand; 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.git.Git;
import com.pmease.commons.persistence.AbstractEntity; import com.pmease.commons.persistence.AbstractEntity;
import com.pmease.gitop.core.Gitop; 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; import com.pmease.gitop.core.manager.RepositoryManager;
@SuppressWarnings("serial") @SuppressWarnings("serial")
@ -55,6 +58,8 @@ public class MergeRequest extends AbstractEntity {
@Column(nullable=false) @Column(nullable=false)
private Status status = Status.OPEN; private Status status = Status.OPEN;
private transient Optional<CheckResult> checkResult;
private transient Boolean merged; private transient Boolean merged;
private transient String mergeBase; private transient String mergeBase;
@ -65,11 +70,13 @@ public class MergeRequest extends AbstractEntity {
private transient MergeRequestUpdate baseUpdate; private transient MergeRequestUpdate baseUpdate;
private transient Collection<User> potentialVoters;
@OneToMany(mappedBy="request") @OneToMany(mappedBy="request")
private Collection<MergeRequestUpdate> updates = new ArrayList<MergeRequestUpdate>(); private Collection<MergeRequestUpdate> updates = new ArrayList<MergeRequestUpdate>();
@OneToMany(mappedBy="request") @OneToMany(mappedBy="request")
private Collection<PendingVote> pendingVotes = new ArrayList<PendingVote>(); private Collection<VoteInvitation> voteInvitations = new ArrayList<VoteInvitation>();
public String getTitle() { public String getTitle() {
return title; return title;
@ -115,12 +122,12 @@ public class MergeRequest extends AbstractEntity {
this.updates = updates; this.updates = updates;
} }
public Collection<PendingVote> getPendingVotes() { public Collection<VoteInvitation> getVoteInvitations() {
return pendingVotes; return voteInvitations;
} }
public void setPendingVotes(Collection<PendingVote> pendingVotes) { public void setVoteInvitations(Collection<VoteInvitation> voteInvitations) {
this.pendingVotes = pendingVotes; this.voteInvitations = voteInvitations;
} }
public Status getStatus() { public Status getStatus() {
@ -239,14 +246,60 @@ public class MergeRequest extends AbstractEntity {
return mergeBase; return mergeBase;
} }
public void requestVote(Collection<User> candidates) { /**
Set<User> pendingUsers = new HashSet<User>(); * Find potential voters for this request. Potential voters will be
for (PendingVote each: getPendingVotes()) * presented with the vote/reject button when they review the request.
pendingUsers.add(each.getReviewer()); * However they may not receive vote invitation.
* <p>
* @return
* a collection of potential voters
*/
public Collection<User> findPotentialVoters() {
check(false);
return getPotentialVoters();
}
pendingUsers.retainAll(candidates); private Collection<User> getPotentialVoters() {
if (potentialVoters == null)
potentialVoters = new HashSet<User>();
return potentialVoters;
}
if (pendingUsers.isEmpty()) { /**
* 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());
// 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>() { User selected = Collections.min(candidates, new Comparator<User>() {
@Override @Override
@ -256,13 +309,48 @@ public class MergeRequest extends AbstractEntity {
}); });
PendingVote pendingVote = new PendingVote(); candidates.remove(selected);
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

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

View File

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

View File

@ -19,7 +19,34 @@ import com.pmease.commons.persistence.AbstractEntity;
}) })
public class Vote extends 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) @ManyToOne(fetch=FetchType.EAGER)
@org.hibernate.annotations.Fetch(FetchMode.SELECT) @org.hibernate.annotations.Fetch(FetchMode.SELECT)

View File

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