use of com.gitblit.models.TicketModel in project gitblit by gitblit.
the class ITicketService method updateComment.
/**
* Updates the text of an ticket comment.
*
* @param ticket
* @param commentId
* the id of the comment to revise
* @param updatedBy
* the author of the updated comment
* @param comment
* the revised comment
* @return the revised ticket if the change was successful
* @since 1.4.0
*/
public final TicketModel updateComment(TicketModel ticket, String commentId, String updatedBy, String comment) {
Change revision = new Change(updatedBy);
revision.comment(comment);
revision.comment.id = commentId;
RepositoryModel repository = repositoryManager.getRepositoryModel(ticket.repository);
TicketModel revisedTicket = updateTicket(repository, ticket.number, revision);
return revisedTicket;
}
use of com.gitblit.models.TicketModel in project gitblit by gitblit.
the class PatchsetReceivePack method preparePatchset.
/**
* Prepares a patchset command.
*
* @param cmd
* @return the patchset command
*/
private PatchsetCommand preparePatchset(ReceiveCommand cmd) {
String branch = getIntegrationBranch(cmd.getRefName());
long number = getTicketId(cmd.getRefName());
TicketModel ticket = null;
if (number > 0 && ticketService.hasTicket(repository, number)) {
ticket = ticketService.getTicket(repository, number);
}
if (ticket == null) {
if (number > 0) {
// requested ticket does not exist
sendError("Sorry, {0} does not have ticket {1,number,0}!", repository.name, number);
sendRejection(cmd, "Invalid ticket number");
return null;
}
} else {
if (ticket.isMerged()) {
// ticket already merged & resolved
Change mergeChange = null;
for (Change change : ticket.changes) {
if (change.isMerge()) {
mergeChange = change;
break;
}
}
if (mergeChange != null) {
sendError("Sorry, {0} already merged {1} from ticket {2,number,0} to {3}!", mergeChange.author, mergeChange.patchset, number, ticket.mergeTo);
}
sendRejection(cmd, "Ticket {0,number,0} already resolved", number);
return null;
} else if (!StringUtils.isEmpty(ticket.mergeTo)) {
// ticket specifies integration branch
branch = ticket.mergeTo;
}
}
final int shortCommitIdLen = settings.getInteger(Keys.web.shortCommitIdLength, 6);
final String shortTipId = cmd.getNewId().getName().substring(0, shortCommitIdLen);
final RevCommit tipCommit = JGitUtils.getCommit(getRepository(), cmd.getNewId().getName());
final String forBranch = branch;
RevCommit mergeBase = null;
Ref forBranchRef = getAdvertisedRefs().get(Constants.R_HEADS + forBranch);
if (forBranchRef == null || forBranchRef.getObjectId() == null) {
// unknown integration branch
sendError("Sorry, there is no integration branch named ''{0}''.", forBranch);
sendRejection(cmd, "Invalid integration branch specified");
return null;
} else {
// determine the merge base for the patchset on the integration branch
String base = JGitUtils.getMergeBase(getRepository(), forBranchRef.getObjectId(), tipCommit.getId());
if (StringUtils.isEmpty(base)) {
sendError("");
sendError("There is no common ancestry between {0} and {1}.", forBranch, shortTipId);
sendError("Please reconsider your proposed integration branch, {0}.", forBranch);
sendError("");
sendRejection(cmd, "no merge base for patchset and {0}", forBranch);
return null;
}
mergeBase = JGitUtils.getCommit(getRepository(), base);
}
// ensure that the patchset can be cleanly merged right now
MergeStatus status = JGitUtils.canMerge(getRepository(), tipCommit.getName(), forBranch, repository.mergeType);
switch(status) {
case ALREADY_MERGED:
sendError("");
sendError("You have already merged this patchset.", forBranch);
sendError("");
sendRejection(cmd, "everything up-to-date");
return null;
case MERGEABLE:
break;
default:
if (ticket == null || requireMergeablePatchset) {
sendError("");
sendError("Your patchset can not be cleanly merged into {0}.", forBranch);
sendError("Please rebase your patchset and push again.");
sendError("NOTE:", number);
sendError("You should push your rebase to refs/for/{0,number,0}", number);
sendError("");
sendError(" git push origin HEAD:refs/for/{0,number,0}", number);
sendError("");
sendRejection(cmd, "patchset not mergeable");
return null;
}
}
// check to see if this commit is already linked to a ticket
if (ticket != null && JGitUtils.getTicketNumberFromCommitBranch(getRepository(), tipCommit) == ticket.number) {
sendError("{0} has already been pushed to ticket {1,number,0}.", shortTipId, ticket.number);
sendRejection(cmd, "everything up-to-date");
return null;
}
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, tipCommit);
PatchsetCommand psCmd;
if (ticket == null) {
/*
* NEW TICKET
*/
Patchset patchset = newPatchset(null, mergeBase.getName(), tipCommit.getName());
int minLength = 10;
int maxLength = 100;
String minTitle = MessageFormat.format(" minimum length of a title is {0} characters.", minLength);
String maxTitle = MessageFormat.format(" maximum length of a title is {0} characters.", maxLength);
if (patchset.commits > 1) {
sendError("");
sendError("You may not create a ''{0}'' branch proposal ticket from {1} commits!", forBranch, patchset.commits);
sendError("");
// display an ellipsized log of the commits being pushed
RevWalk walk = getRevWalk();
walk.reset();
walk.sort(RevSort.TOPO);
int boundary = 3;
int count = 0;
try {
walk.markStart(tipCommit);
walk.markUninteresting(mergeBase);
for (; ; ) {
RevCommit c = walk.next();
if (c == null) {
break;
}
if (count < boundary || count >= (patchset.commits - boundary)) {
walk.parseBody(c);
sendError(" {0} {1}", c.getName().substring(0, shortCommitIdLen), StringUtils.trimString(c.getShortMessage(), 60));
} else if (count == boundary) {
sendError(" ... more commits ...");
}
count++;
}
} catch (IOException e) {
// Should never happen, the core receive process would have
// identified the missing object earlier before we got control.
LOGGER.error("failed to get commit count", e);
} finally {
walk.close();
}
sendError("");
sendError("Possible Solutions:");
sendError("");
int solution = 1;
String forSpec = cmd.getRefName().substring(Constants.R_FOR.length());
if (forSpec.equals("default") || forSpec.equals("new")) {
try {
// determine other possible integration targets
List<String> bases = Lists.newArrayList();
for (Ref ref : getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values()) {
if (!ref.getName().startsWith(Constants.R_TICKET) && !ref.getName().equals(forBranchRef.getName())) {
if (JGitUtils.isMergedInto(getRepository(), ref.getObjectId(), tipCommit)) {
bases.add(Repository.shortenRefName(ref.getName()));
}
}
}
if (!bases.isEmpty()) {
if (bases.size() == 1) {
// suggest possible integration targets
String base = bases.get(0);
sendError("{0}. Propose this change for the ''{1}'' branch.", solution++, base);
sendError("");
sendError(" git push origin HEAD:refs/for/{0}", base);
sendError(" pt propose {0}", base);
sendError("");
} else {
// suggest possible integration targets
sendError("{0}. Propose this change for a different branch.", solution++);
sendError("");
for (String base : bases) {
sendError(" git push origin HEAD:refs/for/{0}", base);
sendError(" pt propose {0}", base);
sendError("");
}
}
}
} catch (IOException e) {
LOGGER.error(null, e);
}
}
sendError("{0}. Squash your changes into a single commit with a meaningful message.", solution++);
sendError("");
sendError("{0}. Open a ticket for your changes and then push your {1} commits to the ticket.", solution++, patchset.commits);
sendError("");
sendError(" git push origin HEAD:refs/for/{id}");
sendError(" pt propose {id}");
sendError("");
sendRejection(cmd, "too many commits");
return null;
}
// require a reasonable title/subject
String title = tipCommit.getFullMessage().trim().split("\n")[0];
if (title.length() < minLength) {
// reject, title too short
sendError("");
sendError("Please supply a longer title in your commit message!");
sendError("");
sendError(minTitle);
sendError(maxTitle);
sendError("");
sendRejection(cmd, "ticket title is too short [{0}/{1}]", title.length(), maxLength);
return null;
}
if (title.length() > maxLength) {
// reject, title too long
sendError("");
sendError("Please supply a more concise title in your commit message!");
sendError("");
sendError(minTitle);
sendError(maxTitle);
sendError("");
sendRejection(cmd, "ticket title is too long [{0}/{1}]", title.length(), maxLength);
return null;
}
// assign new id
long ticketId = ticketService.assignNewId(repository);
// create the patchset command
psCmd = new PatchsetCommand(user.username, patchset);
psCmd.newTicket(tipCommit, forBranch, ticketId, cmd.getRefName());
} else {
/*
* EXISTING TICKET
*/
Patchset patchset = newPatchset(ticket, mergeBase.getName(), tipCommit.getName());
psCmd = new PatchsetCommand(user.username, patchset);
psCmd.updateTicket(tipCommit, forBranch, ticket, cmd.getRefName());
}
// confirm user can push the patchset
boolean pushPermitted = ticket == null || !ticket.hasPatchsets() || ticket.isAuthor(user.username) || ticket.isPatchsetAuthor(user.username) || ticket.isResponsible(user.username) || user.canPush(repository);
switch(psCmd.getPatchsetType()) {
case Proposal:
// proposals (first patchset) are always acceptable
break;
case FastForward:
// patchset updates must be permitted
if (!pushPermitted) {
// reject
sendError("");
sendError("To push a patchset to this ticket one of the following must be true:");
sendError(" 1. you created the ticket");
sendError(" 2. you created the first patchset");
sendError(" 3. you are specified as responsible for the ticket");
sendError(" 4. you have push (RW) permissions to {0}", repository.name);
sendError("");
sendRejection(cmd, "not permitted to push to ticket {0,number,0}", ticket.number);
return null;
}
break;
default:
// non-fast-forward push
if (!pushPermitted) {
// reject
sendRejection(cmd, "non-fast-forward ({0})", psCmd.getPatchsetType());
return null;
}
break;
}
Change change = psCmd.getChange();
change.pendingLinks = ticketLinks;
return psCmd;
}
use of com.gitblit.models.TicketModel in project gitblit by gitblit.
the class PatchsetReceivePack method processReferencedTickets.
/**
* Automatically closes open tickets that have been merged to their integration
* branch by a client and adds references to tickets if made in the commit message.
*
* @param cmd
*/
private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
Map<Long, TicketModel> mergedTickets = new LinkedHashMap<Long, TicketModel>();
final RevWalk rw = getRevWalk();
try {
rw.reset();
rw.markStart(rw.parseCommit(cmd.getNewId()));
if (!ObjectId.zeroId().equals(cmd.getOldId())) {
rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
}
RevCommit c;
while ((c = rw.next()) != null) {
rw.parseBody(c);
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
if (ticketLinks == null) {
continue;
}
for (TicketLink link : ticketLinks) {
if (mergedTickets.containsKey(link.targetTicketId)) {
continue;
}
TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
if (ticket == null) {
continue;
}
String integrationBranch;
if (StringUtils.isEmpty(ticket.mergeTo)) {
// unspecified integration branch
integrationBranch = null;
} else {
// specified integration branch
integrationBranch = Constants.R_HEADS + ticket.mergeTo;
}
Change change;
Patchset patchset = null;
String mergeSha = c.getName();
String mergeTo = Repository.shortenRefName(cmd.getRefName());
if (link.action == TicketAction.Commit) {
//A commit can reference a ticket in any branch even if the ticket is closed.
//This allows developers to identify and communicate related issues
change = new Change(user.username);
change.referenceCommit(mergeSha);
} else {
// ticket must be open and, if specified, the ref must match the integration branch
if (ticket.isClosed() || (integrationBranch != null && !integrationBranch.equals(cmd.getRefName()))) {
continue;
}
String baseRef = PatchsetCommand.getBasePatchsetBranch(ticket.number);
boolean knownPatchset = false;
Set<Ref> refs = getRepository().getAllRefsByPeeledObjectId().get(c.getId());
if (refs != null) {
for (Ref ref : refs) {
if (ref.getName().startsWith(baseRef)) {
knownPatchset = true;
break;
}
}
}
if (knownPatchset) {
// identify merged patchset by the patchset tip
for (Patchset ps : ticket.getPatchsets()) {
if (ps.tip.equals(mergeSha)) {
patchset = ps;
break;
}
}
if (patchset == null) {
// should not happen - unless ticket has been hacked
sendError("Failed to find the patchset for {0} in ticket {1,number,0}?!", mergeSha, ticket.number);
continue;
}
// create a new change
change = new Change(user.username);
} else {
// new patchset pushed by user
String base = cmd.getOldId().getName();
patchset = newPatchset(ticket, base, mergeSha);
PatchsetCommand psCmd = new PatchsetCommand(user.username, patchset);
psCmd.updateTicket(c, mergeTo, ticket, null);
// create a ticket patchset ref
updateRef(psCmd.getPatchsetBranch(), c.getId(), patchset.type);
RefUpdate ru = updateRef(psCmd.getTicketBranch(), c.getId(), patchset.type);
updateReflog(ru);
// create a change from the patchset command
change = psCmd.getChange();
}
// set the common change data about the merge
change.setField(Field.status, Status.Merged);
change.setField(Field.mergeSha, mergeSha);
change.setField(Field.mergeTo, mergeTo);
if (StringUtils.isEmpty(ticket.responsible)) {
// unassigned tickets are assigned to the closer
change.setField(Field.responsible, user.username);
}
}
ticket = ticketService.updateTicket(repository, ticket.number, change);
if (ticket != null) {
sendInfo("");
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
switch(link.action) {
case Commit:
{
sendInfo("referenced by push of {0} to {1}", c.getName(), mergeTo);
}
break;
case Close:
{
sendInfo("closed by push of {0} to {1}", patchset, mergeTo);
mergedTickets.put(ticket.number, ticket);
}
break;
default:
{
}
}
sendInfo(ticketService.getTicketUrl(ticket));
sendInfo("");
} else {
String shortid = mergeSha.substring(0, settings.getInteger(Keys.web.shortCommitIdLength, 6));
switch(link.action) {
case Commit:
{
sendError("FAILED to reference ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);
}
break;
case Close:
{
sendError("FAILED to close ticket {0,number,0} by push of {1}", link.targetTicketId, shortid);
}
break;
default:
{
}
}
}
}
}
} catch (IOException e) {
LOGGER.error("Can't scan for changes to reference or close", e);
} finally {
rw.reset();
}
return mergedTickets.values();
}
use of com.gitblit.models.TicketModel in project gitblit by gitblit.
the class GitblitReceivePack method processReferencedTickets.
/**
* Automatically closes open tickets and adds references to tickets if made in the commit message.
*
* @param cmd
*/
private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
Map<Long, TicketModel> changedTickets = new LinkedHashMap<Long, TicketModel>();
final RevWalk rw = getRevWalk();
try {
rw.reset();
rw.markStart(rw.parseCommit(cmd.getNewId()));
if (!ObjectId.zeroId().equals(cmd.getOldId())) {
rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
}
RevCommit c;
while ((c = rw.next()) != null) {
rw.parseBody(c);
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
if (ticketLinks == null) {
continue;
}
for (TicketLink link : ticketLinks) {
TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
if (ticket == null) {
continue;
}
Change change = null;
String commitSha = c.getName();
String branchName = Repository.shortenRefName(cmd.getRefName());
switch(link.action) {
case Commit:
{
//A commit can reference a ticket in any branch even if the ticket is closed.
//This allows developers to identify and communicate related issues
change = new Change(user.username);
change.referenceCommit(commitSha);
}
break;
case Close:
{
// As this isn't a patchset theres no merging taking place when closing a ticket
if (ticket.isClosed()) {
continue;
}
change = new Change(user.username);
change.setField(Field.status, Status.Fixed);
if (StringUtils.isEmpty(ticket.responsible)) {
// unassigned tickets are assigned to the closer
change.setField(Field.responsible, user.username);
}
}
default:
{
//No action
}
break;
}
if (change != null) {
ticket = ticketService.updateTicket(repository, ticket.number, change);
}
if (ticket != null) {
sendInfo("");
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
switch(link.action) {
case Commit:
{
sendInfo("referenced by push of {0} to {1}", commitSha, branchName);
changedTickets.put(ticket.number, ticket);
}
break;
case Close:
{
sendInfo("closed by push of {0} to {1}", commitSha, branchName);
changedTickets.put(ticket.number, ticket);
}
break;
default:
{
}
}
sendInfo(ticketService.getTicketUrl(ticket));
sendInfo("");
} else {
switch(link.action) {
case Commit:
{
sendError("FAILED to reference ticket {0} by push of {1}", link.targetTicketId, commitSha);
}
break;
case Close:
{
sendError("FAILED to close ticket {0} by push of {1}", link.targetTicketId, commitSha);
}
break;
default:
{
}
}
}
}
}
} catch (IOException e) {
LOGGER.error("Can't scan for changes to reference or close", e);
} finally {
rw.reset();
}
return changedTickets.values();
}
use of com.gitblit.models.TicketModel in project gitblit by gitblit.
the class TicketPage method createMergePanel.
/**
* Adds a merge panel for the patchset to the markup container. The panel
* may just a message if the patchset can not be merged.
*
* @param c
* @param user
* @param repository
*/
protected Component createMergePanel(UserModel user, RepositoryModel repository) {
Patchset patchset = ticket.getCurrentPatchset();
if (patchset == null) {
// no patchset to merge
return new Label("mergePanel");
}
boolean allowMerge;
if (repository.requireApproval) {
// repository requires approval
allowMerge = ticket.isOpen() && ticket.isApproved(patchset);
} else {
// vetoes are binding
allowMerge = ticket.isOpen() && !ticket.isVetoed(patchset);
}
MergeStatus mergeStatus = JGitUtils.canMerge(getRepository(), patchset.tip, ticket.mergeTo, repository.mergeType);
if (allowMerge) {
if (MergeStatus.MERGEABLE == mergeStatus) {
// patchset can be cleanly merged to integration branch OR has already been merged
Fragment mergePanel = new Fragment("mergePanel", "mergeableFragment", this);
mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetMergeable"), ticket.mergeTo)));
if (user.canPush(repository)) {
// user can merge locally
SimpleAjaxLink<String> mergeButton = new SimpleAjaxLink<String>("mergeButton", Model.of(getString("gb.merge"))) {
private static final long serialVersionUID = 1L;
@Override
public void onClick(AjaxRequestTarget target) {
// ensure the patchset is still current AND not vetoed
Patchset patchset = ticket.getCurrentPatchset();
final TicketModel refreshedTicket = app().tickets().getTicket(getRepositoryModel(), ticket.number);
if (patchset.equals(refreshedTicket.getCurrentPatchset())) {
// patchset is current, check for recent veto
if (!refreshedTicket.isVetoed(patchset)) {
// patchset is not vetoed
// execute the merge using the ticket service
app().tickets().exec(new Runnable() {
@Override
public void run() {
PatchsetReceivePack rp = new PatchsetReceivePack(app().gitblit(), getRepository(), getRepositoryModel(), GitBlitWebSession.get().getUser());
MergeStatus result = rp.merge(refreshedTicket);
if (MergeStatus.MERGED == result) {
// notify participants and watchers
rp.sendAll();
} else {
// merge failure
String msg = MessageFormat.format("Failed to merge ticket {0,number,0}: {1}", ticket.number, result.name());
logger.error(msg);
GitBlitWebSession.get().cacheErrorMessage(msg);
}
}
});
} else {
// vetoed patchset
String msg = MessageFormat.format("Can not merge ticket {0,number,0}, patchset {1,number,0} has been vetoed!", ticket.number, patchset.number);
GitBlitWebSession.get().cacheErrorMessage(msg);
logger.error(msg);
}
} else {
// not current patchset
String msg = MessageFormat.format("Can not merge ticket {0,number,0}, the patchset has been updated!", ticket.number);
GitBlitWebSession.get().cacheErrorMessage(msg);
logger.error(msg);
}
redirectTo(TicketsPage.class, getPageParameters());
}
};
mergePanel.add(mergeButton);
Component instructions = getMergeInstructions(user, repository, "mergeMore", "gb.patchsetMergeableMore");
mergePanel.add(instructions);
} else {
mergePanel.add(new Label("mergeButton").setVisible(false));
mergePanel.add(new Label("mergeMore").setVisible(false));
}
return mergePanel;
} else if (MergeStatus.ALREADY_MERGED == mergeStatus) {
// patchset already merged
Fragment mergePanel = new Fragment("mergePanel", "alreadyMergedFragment", this);
mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetAlreadyMerged"), ticket.mergeTo)));
return mergePanel;
} else if (MergeStatus.MISSING_INTEGRATION_BRANCH == mergeStatus) {
// target/integration branch is missing
Fragment mergePanel = new Fragment("mergePanel", "notMergeableFragment", this);
mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
mergePanel.add(new Label("mergeMore", MessageFormat.format(getString("gb.missingIntegrationBranchMore"), ticket.mergeTo)));
return mergePanel;
} else {
// patchset can not be cleanly merged
Fragment mergePanel = new Fragment("mergePanel", "notMergeableFragment", this);
mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
if (user.canPush(repository)) {
// user can merge locally
Component instructions = getMergeInstructions(user, repository, "mergeMore", "gb.patchsetNotMergeableMore");
mergePanel.add(instructions);
} else {
mergePanel.add(new Label("mergeMore").setVisible(false));
}
return mergePanel;
}
} else {
// merge not allowed
if (MergeStatus.ALREADY_MERGED == mergeStatus) {
// patchset already merged
Fragment mergePanel = new Fragment("mergePanel", "alreadyMergedFragment", this);
mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetAlreadyMerged"), ticket.mergeTo)));
return mergePanel;
} else if (ticket.isVetoed(patchset)) {
// patchset has been vetoed
Fragment mergePanel = new Fragment("mergePanel", "vetoedFragment", this);
mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
return mergePanel;
} else if (repository.requireApproval) {
// patchset has been not been approved for merge
Fragment mergePanel = new Fragment("mergePanel", "notApprovedFragment", this);
mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotApproved"), ticket.mergeTo)));
mergePanel.add(new Label("mergeMore", MessageFormat.format(getString("gb.patchsetNotApprovedMore"), ticket.mergeTo)));
return mergePanel;
} else {
// other case
return new Label("mergePanel");
}
}
}
Aggregations