Search in sources :

Example 1 with MergeStatus

use of com.gitblit.utils.JGitUtils.MergeStatus 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");
        }
    }
}
Also used : PatchsetReceivePack(com.gitblit.git.PatchsetReceivePack) Label(org.apache.wicket.markup.html.basic.Label) TicketLabel(com.gitblit.tickets.TicketLabel) Patchset(com.gitblit.models.TicketModel.Patchset) TicketModel(com.gitblit.models.TicketModel) Fragment(org.apache.wicket.markup.html.panel.Fragment) AjaxRequestTarget(org.apache.wicket.ajax.AjaxRequestTarget) SimpleAjaxLink(com.gitblit.wicket.panels.SimpleAjaxLink) MergeStatus(com.gitblit.utils.JGitUtils.MergeStatus) ShockWaveComponent(com.gitblit.wicket.panels.ShockWaveComponent) Component(org.apache.wicket.Component)

Example 2 with MergeStatus

use of com.gitblit.utils.JGitUtils.MergeStatus 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;
}
Also used : TicketModel(com.gitblit.models.TicketModel) Patchset(com.gitblit.models.TicketModel.Patchset) Change(com.gitblit.models.TicketModel.Change) IOException(java.io.IOException) RevWalk(org.eclipse.jgit.revwalk.RevWalk) Ref(org.eclipse.jgit.lib.Ref) MergeStatus(com.gitblit.utils.JGitUtils.MergeStatus) TicketLink(com.gitblit.models.TicketModel.TicketLink) RevCommit(org.eclipse.jgit.revwalk.RevCommit)

Aggregations

TicketModel (com.gitblit.models.TicketModel)2 Patchset (com.gitblit.models.TicketModel.Patchset)2 MergeStatus (com.gitblit.utils.JGitUtils.MergeStatus)2 PatchsetReceivePack (com.gitblit.git.PatchsetReceivePack)1 Change (com.gitblit.models.TicketModel.Change)1 TicketLink (com.gitblit.models.TicketModel.TicketLink)1 TicketLabel (com.gitblit.tickets.TicketLabel)1 ShockWaveComponent (com.gitblit.wicket.panels.ShockWaveComponent)1 SimpleAjaxLink (com.gitblit.wicket.panels.SimpleAjaxLink)1 IOException (java.io.IOException)1 Component (org.apache.wicket.Component)1 AjaxRequestTarget (org.apache.wicket.ajax.AjaxRequestTarget)1 Label (org.apache.wicket.markup.html.basic.Label)1 Fragment (org.apache.wicket.markup.html.panel.Fragment)1 Ref (org.eclipse.jgit.lib.Ref)1 RevCommit (org.eclipse.jgit.revwalk.RevCommit)1 RevWalk (org.eclipse.jgit.revwalk.RevWalk)1