Paul Martin
2016-04-27 c2188a840bc4153ae92112b04b2e06a90d3944aa
commit | author | age
5e3521 1 /*
JM 2  * Copyright 2014 gitblit.com.
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may obtain a copy of the License at
7  *
8  *     http://www.apache.org/licenses/LICENSE-2.0
9  *
10  * Unless required by applicable law or agreed to in writing, software
11  * distributed under the License is distributed on an "AS IS" BASIS,
12  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 package com.gitblit.wicket.pages;
17
cd7e4f 18 import java.io.IOException;
5e3521 19 import java.text.DateFormat;
JM 20 import java.text.MessageFormat;
21 import java.text.SimpleDateFormat;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Calendar;
25 import java.util.Collections;
26 import java.util.Date;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.Set;
30 import java.util.TimeZone;
31 import java.util.TreeSet;
32
33 import javax.servlet.http.HttpServletRequest;
34
35 import org.apache.wicket.AttributeModifier;
36 import org.apache.wicket.Component;
37 import org.apache.wicket.MarkupContainer;
38 import org.apache.wicket.PageParameters;
39 import org.apache.wicket.RestartResponseException;
40 import org.apache.wicket.ajax.AjaxRequestTarget;
41 import org.apache.wicket.behavior.SimpleAttributeModifier;
42 import org.apache.wicket.markup.html.basic.Label;
43 import org.apache.wicket.markup.html.image.ContextImage;
44 import org.apache.wicket.markup.html.link.BookmarkablePageLink;
45 import org.apache.wicket.markup.html.link.ExternalLink;
cd7e4f 46 import org.apache.wicket.markup.html.link.Link;
PM 47 import org.apache.wicket.markup.html.pages.RedirectPage;
5e3521 48 import org.apache.wicket.markup.html.panel.Fragment;
JM 49 import org.apache.wicket.markup.repeater.Item;
50 import org.apache.wicket.markup.repeater.data.DataView;
51 import org.apache.wicket.markup.repeater.data.ListDataProvider;
52 import org.apache.wicket.model.Model;
cd7e4f 53 import org.apache.wicket.protocol.http.RequestUtils;
5e3521 54 import org.apache.wicket.protocol.http.WebRequest;
JM 55 import org.eclipse.jgit.diff.DiffEntry.ChangeType;
56 import org.eclipse.jgit.lib.PersonIdent;
57 import org.eclipse.jgit.lib.Ref;
58 import org.eclipse.jgit.lib.Repository;
59 import org.eclipse.jgit.revwalk.RevCommit;
60
61 import com.gitblit.Constants;
62 import com.gitblit.Constants.AccessPermission;
c211e9 63 import com.gitblit.Constants.AuthorizationControl;
5e3521 64 import com.gitblit.Keys;
JM 65 import com.gitblit.git.PatchsetCommand;
66 import com.gitblit.git.PatchsetReceivePack;
67 import com.gitblit.models.PathModel.PathChangeModel;
68 import com.gitblit.models.RegistrantAccessPermission;
69 import com.gitblit.models.RepositoryModel;
b236ce 70 import com.gitblit.models.RepositoryUrl;
5e3521 71 import com.gitblit.models.SubmoduleModel;
JM 72 import com.gitblit.models.TicketModel;
73 import com.gitblit.models.TicketModel.Change;
74 import com.gitblit.models.TicketModel.CommentSource;
75 import com.gitblit.models.TicketModel.Field;
76 import com.gitblit.models.TicketModel.Patchset;
77 import com.gitblit.models.TicketModel.PatchsetType;
78 import com.gitblit.models.TicketModel.Review;
79 import com.gitblit.models.TicketModel.Score;
80 import com.gitblit.models.TicketModel.Status;
81 import com.gitblit.models.UserModel;
82 import com.gitblit.tickets.TicketIndexer.Lucene;
83 import com.gitblit.tickets.TicketLabel;
84 import com.gitblit.tickets.TicketMilestone;
85 import com.gitblit.tickets.TicketResponsible;
b236ce 86 import com.gitblit.utils.ArrayUtils;
5e3521 87 import com.gitblit.utils.JGitUtils;
JM 88 import com.gitblit.utils.JGitUtils.MergeStatus;
cd7e4f 89 import com.gitblit.utils.CommitCache;
5e3521 90 import com.gitblit.utils.MarkdownUtils;
cd7e4f 91 import com.gitblit.utils.RefLogUtils;
5e3521 92 import com.gitblit.utils.StringUtils;
JM 93 import com.gitblit.utils.TimeUtils;
94 import com.gitblit.wicket.GitBlitWebSession;
fdd82f 95 import com.gitblit.wicket.TicketsUI;
5e3521 96 import com.gitblit.wicket.WicketUtils;
a59232 97 import com.gitblit.wicket.panels.AvatarImage;
cd7e4f 98 import com.gitblit.wicket.panels.BasePanel.JavascriptEventConfirmation;
5e3521 99 import com.gitblit.wicket.panels.BasePanel.JavascriptTextPrompt;
JM 100 import com.gitblit.wicket.panels.CommentPanel;
101 import com.gitblit.wicket.panels.DiffStatPanel;
102 import com.gitblit.wicket.panels.IconAjaxLink;
103 import com.gitblit.wicket.panels.LinkPanel;
104 import com.gitblit.wicket.panels.ShockWaveComponent;
105 import com.gitblit.wicket.panels.SimpleAjaxLink;
106
107 /**
108  * The ticket page handles viewing and updating a ticket.
109  *
110  * @author James Moger
111  *
112  */
fdd82f 113 public class TicketPage extends RepositoryPage {
5e3521 114
JM 115     static final String NIL = "<nil>";
116
117     static final String ESC_NIL = StringUtils.escapeForHtml(NIL,  false);
118
119     final int avatarWidth = 40;
120
121     final TicketModel ticket;
122
123     public TicketPage(PageParameters params) {
124         super(params);
125
126         final UserModel user = GitBlitWebSession.get().getUser() == null ? UserModel.ANONYMOUS : GitBlitWebSession.get().getUser();
127         final RepositoryModel repository = getRepositoryModel();
128         final String id = WicketUtils.getObject(params);
129         long ticketId = Long.parseLong(id);
130         ticket = app().tickets().getTicket(repository, ticketId);
131
132         if (ticket == null) {
133             // ticket not found
134             throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName));
135         }
136
137         final List<Change> revisions = new ArrayList<Change>();
138         List<Change> comments = new ArrayList<Change>();
139         List<Change> statusChanges = new ArrayList<Change>();
140         List<Change> discussion = new ArrayList<Change>();
141         for (Change change : ticket.changes) {
142             if (change.hasComment() || (change.isStatusChange() && (change.getStatus() != Status.New))) {
143                 discussion.add(change);
144             }
145             if (change.hasComment()) {
146                 comments.add(change);
147             }
148             if (change.hasPatchset()) {
149                 revisions.add(change);
150             }
151             if (change.isStatusChange() && !change.hasPatchset()) {
152                 statusChanges.add(change);
153             }
154         }
155
156         final Change currentRevision = revisions.isEmpty() ? null : revisions.get(revisions.size() - 1);
157         final Patchset currentPatchset = ticket.getCurrentPatchset();
158
159         /*
160          * TICKET HEADER
161          */
162         String href = urlFor(TicketsPage.class, params).toString();
163         add(new ExternalLink("ticketNumber", href, "#" + ticket.number));
164         Label headerStatus = new Label("headerStatus", ticket.status.toString());
fdd82f 165         WicketUtils.setCssClass(headerStatus, TicketsUI.getLozengeClass(ticket.status, false));
5e3521 166         add(headerStatus);
JM 167         add(new Label("ticketTitle", ticket.title));
168         if (currentPatchset == null) {
169             add(new Label("diffstat").setVisible(false));
170         } else {
171             // calculate the current diffstat of the patchset
172             add(new DiffStatPanel("diffstat", ticket.insertions, ticket.deletions));
173         }
174
175
176         /*
177          * TAB TITLES
178          */
179         add(new Label("commentCount", "" + comments.size()).setVisible(!comments.isEmpty()));
180         add(new Label("commitCount", "" + (currentPatchset == null ? 0 : currentPatchset.commits)).setVisible(currentPatchset != null));
181
182
183         /*
184          * TICKET AUTHOR and DATE (DISCUSSION TAB)
185          */
186         UserModel createdBy = app().users().getUserModel(ticket.createdBy);
187         if (createdBy == null) {
188             add(new Label("whoCreated", ticket.createdBy));
189         } else {
190             add(new LinkPanel("whoCreated", null, createdBy.getDisplayName(),
191                     UserPage.class, WicketUtils.newUsernameParameter(createdBy.username)));
192         }
193
194         if (ticket.isProposal()) {
195             // clearly indicate this is a change ticket
196             add(new Label("creationMessage", getString("gb.proposedThisChange")));
197         } else {
198             // standard ticket
199             add(new Label("creationMessage", getString("gb.createdThisTicket")));
200         }
201
202         String dateFormat = app().settings().getString(Keys.web.datestampLongFormat, "EEEE, MMMM d, yyyy");
203         String timestampFormat = app().settings().getString(Keys.web.datetimestampLongFormat, "EEEE, MMMM d, yyyy");
204         final TimeZone timezone = getTimeZone();
205         final DateFormat df = new SimpleDateFormat(dateFormat);
206         df.setTimeZone(timezone);
207         final DateFormat tsf = new SimpleDateFormat(timestampFormat);
208         tsf.setTimeZone(timezone);
209         final Calendar cal = Calendar.getInstance(timezone);
210
211         String fuzzydate;
212         TimeUtils tu = getTimeUtils();
213         Date createdDate = ticket.created;
214         if (TimeUtils.isToday(createdDate, timezone)) {
215             fuzzydate = tu.today();
216         } else if (TimeUtils.isYesterday(createdDate, timezone)) {
217             fuzzydate = tu.yesterday();
218         } else {
219             // calculate a fuzzy time ago date
220             cal.setTime(createdDate);
221             cal.set(Calendar.HOUR_OF_DAY, 0);
222             cal.set(Calendar.MINUTE, 0);
223             cal.set(Calendar.SECOND, 0);
224             cal.set(Calendar.MILLISECOND, 0);
225             createdDate = cal.getTime();
226             fuzzydate = getTimeUtils().timeAgo(createdDate);
227         }
228         Label when = new Label("whenCreated", fuzzydate + ", " + df.format(createdDate));
229         WicketUtils.setHtmlTooltip(when, tsf.format(ticket.created));
230         add(when);
231
232         String exportHref = urlFor(ExportTicketPage.class, params).toString();
233         add(new ExternalLink("exportJson", exportHref, "json"));
234
235
236         /*
237          * RESPONSIBLE (DISCUSSION TAB)
238          */
239         if (StringUtils.isEmpty(ticket.responsible)) {
240             add(new Label("responsible"));
241         } else {
242             UserModel responsible = app().users().getUserModel(ticket.responsible);
243             if (responsible == null) {
244                 add(new Label("responsible", ticket.responsible));
245             } else {
246                 add(new LinkPanel("responsible", null, responsible.getDisplayName(),
247                         UserPage.class, WicketUtils.newUsernameParameter(responsible.username)));
248             }
249         }
250
251         /*
252          * MILESTONE PROGRESS (DISCUSSION TAB)
253          */
254         if (StringUtils.isEmpty(ticket.milestone)) {
255             add(new Label("milestone"));
256         } else {
257             // link to milestone query
270e9e 258             TicketMilestone tm = app().tickets().getMilestone(repository, ticket.milestone);
JM 259             if (tm == null) {
260                 tm = new TicketMilestone(ticket.milestone);
261             }
e03317 262             PageParameters milestoneParameters;
270e9e 263             if (tm.isOpen()) {
e03317 264                 milestoneParameters = WicketUtils.newOpenTicketsParameter(repositoryName);
JM 265             } else {
266                 milestoneParameters = WicketUtils.newRepositoryParameter(repositoryName);
267             }
5e3521 268             milestoneParameters.put(Lucene.milestone.name(), ticket.milestone);
JM 269             int progress = 0;
270             int open = 0;
271             int closed = 0;
270e9e 272             if (tm != null) {
JM 273                 progress = tm.getProgress();
274                 open = tm.getOpenTickets();
275                 closed = tm.getClosedTickets();
5e3521 276             }
JM 277
278             Fragment milestoneProgress = new Fragment("milestone", "milestoneProgressFragment", this);
279             milestoneProgress.add(new LinkPanel("link", null, ticket.milestone, TicketsPage.class, milestoneParameters));
280             Label label = new Label("progress");
281             WicketUtils.setCssStyle(label, "width:" + progress + "%;");
282             milestoneProgress.add(label);
9f50fd 283             WicketUtils.setHtmlTooltip(milestoneProgress, MessageFormat.format(getString("gb.milestoneProgress"), open, closed));
5e3521 284             add(milestoneProgress);
JM 285         }
286
287
288         /*
289          * TICKET DESCRIPTION (DISCUSSION TAB)
290          */
291         String desc;
292         if (StringUtils.isEmpty(ticket.body)) {
293             desc = getString("gb.noDescriptionGiven");
294         } else {
d07b16 295             String bugtraq = bugtraqProcessor().processText(getRepository(), repositoryName, ticket.body);
a59627 296             String html = MarkdownUtils.transformGFM(app().settings(), bugtraq, ticket.repository);
JM 297             String safeHtml = app().xssFilter().relaxed(html);
298             desc = safeHtml;
5e3521 299         }
JM 300         add(new Label("ticketDescription", desc).setEscapeModelStrings(false));
301
302
303         /*
304          * PARTICIPANTS (DISCUSSION TAB)
305          */
306         if (app().settings().getBoolean(Keys.web.allowGravatar, true)) {
307             // gravatar allowed
308             List<String> participants = ticket.getParticipants();
309             add(new Label("participantsLabel", MessageFormat.format(getString(participants.size() > 1 ? "gb.nParticipants" : "gb.oneParticipant"),
310                     "<b>" + participants.size() + "</b>")).setEscapeModelStrings(false));
311             ListDataProvider<String> participantsDp = new ListDataProvider<String>(participants);
312             DataView<String> participantsView = new DataView<String>("participants", participantsDp) {
313                 private static final long serialVersionUID = 1L;
314
315                 @Override
316                 public void populateItem(final Item<String> item) {
317                     String username = item.getModelObject();
318                     UserModel user = app().users().getUserModel(username);
319                     if (user == null) {
320                         user = new UserModel(username);
321                     }
b57b9e 322                     item.add(new AvatarImage("participant", user.getDisplayName(),
5e3521 323                             user.emailAddress, null, 25, true));
JM 324                 }
325             };
326             add(participantsView);
327         } else {
328             // gravatar prohibited
329             add(new Label("participantsLabel").setVisible(false));
330             add(new Label("participants").setVisible(false));
331         }
332
333
334         /*
335          * LARGE STATUS INDICATOR WITH ICON (DISCUSSION TAB->SIDE BAR)
336          */
337         Fragment ticketStatus = new Fragment("ticketStatus", "ticketStatusFragment", this);
fdd82f 338         Label ticketIcon = TicketsUI.getStateIcon("ticketIcon", ticket);
5e3521 339         ticketStatus.add(ticketIcon);
JM 340         ticketStatus.add(new Label("ticketStatus", ticket.status.toString()));
fdd82f 341         WicketUtils.setCssClass(ticketStatus, TicketsUI.getLozengeClass(ticket.status, false));
5e3521 342         add(ticketStatus);
JM 343
344
345         /*
346          * UPDATE FORM (DISCUSSION TAB)
347          */
7ca053 348         if (user.canEdit(ticket, repository) && app().tickets().isAcceptingTicketUpdates(repository)) {
cc1c3f 349             if (user.canAdmin(ticket, repository) && ticket.isOpen()) {
85775a 350                 /*
JM 351                  * OPEN TICKET
352                  */
353                 Fragment controls = new Fragment("controls", "openControlsFragment", this);
5e3521 354
85775a 355                 /*
JM 356                  * STATUS
357                  */
358                 List<Status> choices = new ArrayList<Status>();
359                 if (ticket.isProposal()) {
360                     choices.addAll(Arrays.asList(TicketModel.Status.proposalWorkflow));
361                 } else if (ticket.isBug()) {
362                     choices.addAll(Arrays.asList(TicketModel.Status.bugWorkflow));
363                 } else {
364                     choices.addAll(Arrays.asList(TicketModel.Status.requestWorkflow));
5e3521 365                 }
85775a 366                 choices.remove(ticket.status);
5e3521 367
85775a 368                 ListDataProvider<Status> workflowDp = new ListDataProvider<Status>(choices);
JM 369                 DataView<Status> statusView = new DataView<Status>("newStatus", workflowDp) {
370                     private static final long serialVersionUID = 1L;
5e3521 371
85775a 372                     @Override
JM 373                     public void populateItem(final Item<Status> item) {
374                         SimpleAjaxLink<Status> link = new SimpleAjaxLink<Status>("link", item.getModel()) {
5e3521 375
85775a 376                             private static final long serialVersionUID = 1L;
5e3521 377
85775a 378                             @Override
JM 379                             public void onClick(AjaxRequestTarget target) {
380                                 Status status = getModel().getObject();
381                                 Change change = new Change(user.username);
382                                 change.setField(Field.status, status);
383                                 if (!ticket.isWatching(user.username)) {
384                                     change.watch(user.username);
5e3521 385                                 }
85775a 386                                 TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
JM 387                                 app().tickets().createNotifier().sendMailing(update);
47a544 388                                 redirectTo(TicketsPage.class, getPageParameters());
5e3521 389                             }
85775a 390                         };
fdd82f 391                         String css = TicketsUI.getStatusClass(item.getModel().getObject());
85775a 392                         WicketUtils.setCssClass(link, css);
JM 393                         item.add(link);
394                     }
395                 };
396                 controls.add(statusView);
5e3521 397
85775a 398                 /*
JM 399                  * RESPONSIBLE LIST
400                  */
401                 Set<String> userlist = new TreeSet<String>(ticket.getParticipants());
c211e9 402                 if (UserModel.ANONYMOUS.canPush(getRepositoryModel())
JM 403                         || AuthorizationControl.AUTHENTICATED == getRepositoryModel().authorizationControl) {
404                     //     authorization is ANONYMOUS or AUTHENTICATED (i.e. all users can be set responsible)
405                     userlist.addAll(app().users().getAllUsernames());
406                 } else {
407                     // authorization is by NAMED users (users with PUSH permission can be set responsible)
408                     for (RegistrantAccessPermission rp : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
805468 409                         if (rp.permission.atLeast(AccessPermission.PUSH)) {
c211e9 410                             userlist.add(rp.registrant);
JM 411                         }
5e3521 412                     }
JM 413                 }
85775a 414                 List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
JM 415                 if (!StringUtils.isEmpty(ticket.responsible)) {
416                     // exclude the current responsible
417                     userlist.remove(ticket.responsible);
5e3521 418                 }
85775a 419                 for (String username : userlist) {
JM 420                     UserModel u = app().users().getUserModel(username);
421                     if (u != null) {
422                         responsibles.add(new TicketResponsible(u));
423                     }
424                 }
425                 Collections.sort(responsibles);
426                 responsibles.add(new TicketResponsible(ESC_NIL, "", ""));
427                 ListDataProvider<TicketResponsible> responsibleDp = new ListDataProvider<TicketResponsible>(responsibles);
428                 DataView<TicketResponsible> responsibleView = new DataView<TicketResponsible>("newResponsible", responsibleDp) {
429                     private static final long serialVersionUID = 1L;
5e3521 430
85775a 431                     @Override
JM 432                     public void populateItem(final Item<TicketResponsible> item) {
433                         SimpleAjaxLink<TicketResponsible> link = new SimpleAjaxLink<TicketResponsible>("link", item.getModel()) {
5e3521 434
85775a 435                             private static final long serialVersionUID = 1L;
JM 436
437                             @Override
438                             public void onClick(AjaxRequestTarget target) {
439                                 TicketResponsible responsible = getModel().getObject();
440                                 Change change = new Change(user.username);
441                                 change.setField(Field.responsible, responsible.username);
442                                 if (!StringUtils.isEmpty(responsible.username)) {
443                                     if (!ticket.isWatching(responsible.username)) {
444                                         change.watch(responsible.username);
445                                     }
446                                 }
447                                 if (!ticket.isWatching(user.username)) {
448                                     change.watch(user.username);
449                                 }
450                                 TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
451                                 app().tickets().createNotifier().sendMailing(update);
47a544 452                                 redirectTo(TicketsPage.class, getPageParameters());
85775a 453                             }
JM 454                         };
455                         item.add(link);
456                     }
457                 };
458                 controls.add(responsibleView);
459
460                 /*
461                  * MILESTONE LIST
462                  */
463                 List<TicketMilestone> milestones = app().tickets().getMilestones(repository, Status.Open);
464                 if (!StringUtils.isEmpty(ticket.milestone)) {
465                     for (TicketMilestone milestone : milestones) {
466                         if (milestone.name.equals(ticket.milestone)) {
467                             milestones.remove(milestone);
468                             break;
469                         }
470                     }
471                 }
472                 milestones.add(new TicketMilestone(ESC_NIL));
473                 ListDataProvider<TicketMilestone> milestoneDp = new ListDataProvider<TicketMilestone>(milestones);
474                 DataView<TicketMilestone> milestoneView = new DataView<TicketMilestone>("newMilestone", milestoneDp) {
475                     private static final long serialVersionUID = 1L;
476
477                     @Override
478                     public void populateItem(final Item<TicketMilestone> item) {
479                         SimpleAjaxLink<TicketMilestone> link = new SimpleAjaxLink<TicketMilestone>("link", item.getModel()) {
480
481                             private static final long serialVersionUID = 1L;
482
483                             @Override
484                             public void onClick(AjaxRequestTarget target) {
485                                 TicketMilestone milestone = getModel().getObject();
486                                 Change change = new Change(user.username);
487                                 if (NIL.equals(milestone.name) || ESC_NIL.equals(milestone.name)) {
488                                     change.setField(Field.milestone, "");
489                                 } else {
490                                     change.setField(Field.milestone, milestone.name);
491                                 }
492                                 if (!ticket.isWatching(user.username)) {
493                                     change.watch(user.username);
494                                 }
495                                 TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
496                                 app().tickets().createNotifier().sendMailing(update);
47a544 497                                 redirectTo(TicketsPage.class, getPageParameters());
85775a 498                             }
JM 499                         };
500                         item.add(link);
501                     }
502                 };
503                 controls.add(milestoneView);
504
505                 String editHref = urlFor(EditTicketPage.class, params).toString();
506                 controls.add(new ExternalLink("editLink", editHref, getString("gb.edit")));
507
508                 add(controls);
509             } else {
510                 /*
511                  * CLOSED TICKET
512                  */
513                 Fragment controls = new Fragment("controls", "closedControlsFragment", this);
514
515                 String editHref = urlFor(EditTicketPage.class, params).toString();
516                 controls.add(new ExternalLink("editLink", editHref, getString("gb.edit")));
517
518                 add(controls);
519             }
5e3521 520         } else {
JM 521             add(new Label("controls").setVisible(false));
522         }
523
524
525         /*
526          * TICKET METADATA
527          */
528         add(new Label("ticketType", ticket.type.toString()));
4a2fb1 529
f9c78c 530         add(new Label("priority", ticket.priority.toString()));
PM 531         add(new Label("severity", ticket.severity.toString()));
4a2fb1 532
5e3521 533         if (StringUtils.isEmpty(ticket.topic)) {
JM 534             add(new Label("ticketTopic").setVisible(false));
535         } else {
536             // process the topic using the bugtraq config to link things
d07b16 537             String topic = bugtraqProcessor().processText(getRepository(), repositoryName, ticket.topic);
a59627 538             String safeTopic = app().xssFilter().relaxed(topic);
JM 539             add(new Label("ticketTopic", safeTopic).setEscapeModelStrings(false));
5e3521 540         }
4a2fb1 541
JM 542
5e3521 543
JM 544
545         /*
546          * VOTERS
547          */
548         List<String> voters = ticket.getVoters();
549         Label votersCount = new Label("votes", "" + voters.size());
550         if (voters.size() == 0) {
551             WicketUtils.setCssClass(votersCount, "badge");
552         } else {
553             WicketUtils.setCssClass(votersCount, "badge badge-info");
554         }
555         add(votersCount);
5fcd95 556         if (user.isAuthenticated && app().tickets().isAcceptingTicketUpdates(repository)) {
5e3521 557             Model<String> model;
JM 558             if (ticket.isVoter(user.username)) {
559                 model = Model.of(getString("gb.removeVote"));
560             } else {
561                 model = Model.of(MessageFormat.format(getString("gb.vote"), ticket.type.toString()));
562             }
563             SimpleAjaxLink<String> link = new SimpleAjaxLink<String>("voteLink", model) {
564
565                 private static final long serialVersionUID = 1L;
566
567                 @Override
568                 public void onClick(AjaxRequestTarget target) {
569                     Change change = new Change(user.username);
570                     if (ticket.isVoter(user.username)) {
571                         change.unvote(user.username);
572                     } else {
573                         change.vote(user.username);
574                     }
575                     app().tickets().updateTicket(repository, ticket.number, change);
47a544 576                     redirectTo(TicketsPage.class, getPageParameters());
5e3521 577                 }
JM 578             };
579             add(link);
580         } else {
581             add(new Label("voteLink").setVisible(false));
582         }
583
584
585         /*
586          * WATCHERS
587          */
588         List<String> watchers = ticket.getWatchers();
589         Label watchersCount = new Label("watchers", "" + watchers.size());
590         if (watchers.size() == 0) {
591             WicketUtils.setCssClass(watchersCount, "badge");
592         } else {
593             WicketUtils.setCssClass(watchersCount, "badge badge-info");
594         }
595         add(watchersCount);
5fcd95 596         if (user.isAuthenticated && app().tickets().isAcceptingTicketUpdates(repository)) {
5e3521 597             Model<String> model;
JM 598             if (ticket.isWatching(user.username)) {
599                 model = Model.of(getString("gb.stopWatching"));
600             } else {
601                 model = Model.of(MessageFormat.format(getString("gb.watch"), ticket.type.toString()));
602             }
603             SimpleAjaxLink<String> link = new SimpleAjaxLink<String>("watchLink", model) {
604
605                 private static final long serialVersionUID = 1L;
606
607                 @Override
608                 public void onClick(AjaxRequestTarget target) {
609                     Change change = new Change(user.username);
610                     if (ticket.isWatching(user.username)) {
611                         change.unwatch(user.username);
612                     } else {
613                         change.watch(user.username);
614                     }
615                     app().tickets().updateTicket(repository, ticket.number, change);
47a544 616                     redirectTo(TicketsPage.class, getPageParameters());
5e3521 617                 }
JM 618             };
619             add(link);
620         } else {
621             add(new Label("watchLink").setVisible(false));
622         }
623
624
625         /*
626          * TOPIC & LABELS (DISCUSSION TAB->SIDE BAR)
627          */
628         ListDataProvider<String> labelsDp = new ListDataProvider<String>(ticket.getLabels());
629         DataView<String> labelsView = new DataView<String>("labels", labelsDp) {
630             private static final long serialVersionUID = 1L;
631
632             @Override
633             public void populateItem(final Item<String> item) {
634                 final String value = item.getModelObject();
635                 Label label = new Label("label", value);
636                 TicketLabel tLabel = app().tickets().getLabel(repository, value);
637                 String background = MessageFormat.format("background-color:{0};", tLabel.color);
638                 label.add(new SimpleAttributeModifier("style", background));
639                 item.add(label);
640             }
641         };
642
643         add(labelsView);
644
645
646         /*
647          * COMMENTS & STATUS CHANGES (DISCUSSION TAB)
648          */
649         if (comments.size() == 0) {
650             add(new Label("discussion").setVisible(false));
651         } else {
652             Fragment discussionFragment = new Fragment("discussion", "discussionFragment", this);
653             ListDataProvider<Change> discussionDp = new ListDataProvider<Change>(discussion);
654             DataView<Change> discussionView = new DataView<Change>("discussion", discussionDp) {
655                 private static final long serialVersionUID = 1L;
656
657                 @Override
658                 public void populateItem(final Item<Change> item) {
659                     final Change entry = item.getModelObject();
660                     if (entry.isMerge()) {
661                         /*
662                          * MERGE
663                          */
664                         String resolvedBy = entry.getString(Field.mergeSha);
665
666                         // identify the merged patch, it is likely the last
667                         Patchset mergedPatch = null;
668                         for (Change c : revisions) {
669                             if (c.patchset.tip.equals(resolvedBy)) {
670                                 mergedPatch = c.patchset;
671                                 break;
672                             }
673                         }
674
675                         String commitLink;
676                         if (mergedPatch == null) {
677                             // shouldn't happen, but just-in-case
678                             int len = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);
679                             commitLink = resolvedBy.substring(0, len);
680                         } else {
681                             // expected result
682                             commitLink = mergedPatch.toString();
683                         }
684
685                         Fragment mergeFragment = new Fragment("entry", "mergeFragment", this);
686                         mergeFragment.add(new LinkPanel("commitLink", null, commitLink,
687                                 CommitPage.class, WicketUtils.newObjectParameter(repositoryName, resolvedBy)));
688                         mergeFragment.add(new Label("toBranch", MessageFormat.format(getString("gb.toBranch"),
689                                 "<b>" + ticket.mergeTo + "</b>")).setEscapeModelStrings(false));
690                         addUserAttributions(mergeFragment, entry, 0);
691                         addDateAttributions(mergeFragment, entry);
692
693                         item.add(mergeFragment);
694                     } else if (entry.isStatusChange()) {
695                         /*
696                          *  STATUS CHANGE
697                          */
698                         Fragment frag = new Fragment("entry", "statusFragment", this);
699                         Label status = new Label("statusChange", entry.getStatus().toString());
fdd82f 700                         String css = TicketsUI.getLozengeClass(entry.getStatus(), false);
5e3521 701                         WicketUtils.setCssClass(status, css);
JM 702                         frag.add(status);
703                         addUserAttributions(frag, entry, avatarWidth);
704                         addDateAttributions(frag, entry);
705                         item.add(frag);
706                     } else {
707                         /*
708                          * COMMENT
709                          */
d07b16 710                         String bugtraq = bugtraqProcessor().processText(getRepository(), repositoryName, entry.comment.text);
887300 711                         String comment = MarkdownUtils.transformGFM(app().settings(), bugtraq, repositoryName);
a59627 712                         String safeComment = app().xssFilter().relaxed(comment);
5e3521 713                         Fragment frag = new Fragment("entry", "commentFragment", this);
JM 714                         Label commentIcon = new Label("commentIcon");
715                         if (entry.comment.src == CommentSource.Email) {
716                             WicketUtils.setCssClass(commentIcon, "iconic-mail");
717                         } else {
718                             WicketUtils.setCssClass(commentIcon, "iconic-comment-alt2-stroke");
719                         }
720                         frag.add(commentIcon);
a59627 721                         frag.add(new Label("comment", safeComment).setEscapeModelStrings(false));
5e3521 722                         addUserAttributions(frag, entry, avatarWidth);
JM 723                         addDateAttributions(frag, entry);
724                         item.add(frag);
725                     }
726                 }
727             };
728             discussionFragment.add(discussionView);
729             add(discussionFragment);
730         }
731
732         /*
733          * ADD COMMENT PANEL
734          */
735         if (UserModel.ANONYMOUS.equals(user)
736                 || !repository.isBare
737                 || repository.isFrozen
738                 || repository.isMirror) {
739
740             // prohibit comments for anonymous users, local working copy repos,
741             // frozen repos, and mirrors
742             add(new Label("newComment").setVisible(false));
743         } else {
744             // permit user to comment
745             Fragment newComment = new Fragment("newComment", "newCommentFragment", this);
b57b9e 746             AvatarImage img = new AvatarImage("newCommentAvatar", user.username, user.emailAddress,
5e3521 747                     "gravatar-round", avatarWidth, true);
JM 748             newComment.add(img);
749             CommentPanel commentPanel = new CommentPanel("commentPanel", user, ticket, null, TicketsPage.class);
750             commentPanel.setRepository(repositoryName);
751             newComment.add(commentPanel);
752             add(newComment);
753         }
754
755
756         /*
757          *  PATCHSET TAB
758          */
759         if (currentPatchset == null) {
31e1bf 760             // no patchset available
d85bfb 761             RepositoryUrl repoUrl = getRepositoryUrl(user, repository);
1590fd 762             boolean canPropose = repoUrl != null && repoUrl.hasPermission() && repoUrl.permission.atLeast(AccessPermission.CLONE) && !UserModel.ANONYMOUS.equals(user);
d85bfb 763             if (ticket.isOpen() && app().tickets().isAcceptingNewPatchsets(repository) && canPropose) {
31e1bf 764                 // ticket & repo will accept a proposal patchset
JM 765                 // show the instructions for proposing a patchset
766                 Fragment changeIdFrag = new Fragment("patchset", "proposeFragment", this);
767                 changeIdFrag.add(new Label("proposeInstructions", MarkdownUtils.transformMarkdown(getString("gb.proposeInstructions"))).setEscapeModelStrings(false));
768                 changeIdFrag.add(new Label("ptWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Barnum")));
d85bfb 769                 changeIdFrag.add(new Label("ptWorkflowSteps", getProposeWorkflow("propose_pt.md", repoUrl.url, ticket.number)).setEscapeModelStrings(false));
31e1bf 770                 changeIdFrag.add(new Label("gitWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Git")));
d85bfb 771                 changeIdFrag.add(new Label("gitWorkflowSteps", getProposeWorkflow("propose_git.md", repoUrl.url, ticket.number)).setEscapeModelStrings(false));
31e1bf 772                 add(changeIdFrag);
JM 773             } else {
4affd0 774                 // explain why you can't propose a patchset
JM 775                 Fragment fragment = new Fragment("patchset", "canNotProposeFragment", this);
776                 String reason = "";
777                 if (ticket.isClosed()) {
778                     reason = getString("gb.ticketIsClosed");
779                 } else if (repository.isMirror) {
780                     reason = getString("gb.repositoryIsMirror");
781                 } else if (repository.isFrozen) {
782                     reason = getString("gb.repositoryIsFrozen");
783                 } else if (!repository.acceptNewPatchsets) {
784                     reason = getString("gb.repositoryDoesNotAcceptPatchsets");
d85bfb 785                 } else if (!canPropose) {
JM 786                     if (UserModel.ANONYMOUS.equals(user)) {
787                         reason = getString("gb.anonymousCanNotPropose");
788                     } else {
789                         reason = getString("gb.youDoNotHaveClonePermission");
790                     }
4affd0 791                 } else {
JM 792                     reason = getString("gb.serverDoesNotAcceptPatchsets");
793                 }
794                 fragment.add(new Label("reason", reason));
795                 add(fragment);
31e1bf 796             }
5e3521 797         } else {
JM 798             // show current patchset
799             Fragment patchsetFrag = new Fragment("patchset", "patchsetFragment", this);
800             patchsetFrag.add(new Label("commitsInPatchset", MessageFormat.format(getString("gb.commitsInPatchsetN"), currentPatchset.number)));
801
a0d5c8 802             patchsetFrag.add(createMergePanel(user, repository));
JM 803
804             if (ticket.isOpen()) {
805                 // current revision
806                 MarkupContainer panel = createPatchsetPanel("panel", repository, user);
807                 patchsetFrag.add(panel);
808                 addUserAttributions(patchsetFrag, currentRevision, avatarWidth);
809                 addUserAttributions(panel, currentRevision, 0);
810                 addDateAttributions(panel, currentRevision);
811             } else {
812                 // current revision
813                 patchsetFrag.add(new Label("panel").setVisible(false));
814             }
5e3521 815
JM 816             // commits
817             List<RevCommit> commits = JGitUtils.getRevLog(getRepository(), currentPatchset.base, currentPatchset.tip);
818             ListDataProvider<RevCommit> commitsDp = new ListDataProvider<RevCommit>(commits);
819             DataView<RevCommit> commitsView = new DataView<RevCommit>("commit", commitsDp) {
820                 private static final long serialVersionUID = 1L;
821
822                 @Override
823                 public void populateItem(final Item<RevCommit> item) {
824                     RevCommit commit = item.getModelObject();
825                     PersonIdent author = commit.getAuthorIdent();
b57b9e 826                     item.add(new AvatarImage("authorAvatar", author.getName(), author.getEmailAddress(), null, 16, false));
5e3521 827                     item.add(new Label("author", commit.getAuthorIdent().getName()));
JM 828                     item.add(new LinkPanel("commitId", null, getShortObjectId(commit.getName()),
829                             CommitPage.class, WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
830                     item.add(new LinkPanel("diff", "link", getString("gb.diff"), CommitDiffPage.class,
831                             WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
832                     item.add(new Label("title", StringUtils.trimString(commit.getShortMessage(), Constants.LEN_SHORTLOG_REFS)));
a59232 833                     item.add(WicketUtils.createDateLabel("commitDate", JGitUtils.getAuthorDate(commit), GitBlitWebSession
5e3521 834                             .get().getTimezone(), getTimeUtils(), false));
JM 835                     item.add(new DiffStatPanel("commitDiffStat", 0, 0, true));
836                 }
837             };
838             patchsetFrag.add(commitsView);
839             add(patchsetFrag);
840         }
841
842
843         /*
844          * ACTIVITY TAB
845          */
846         Fragment revisionHistory = new Fragment("activity", "activityFragment", this);
847         List<Change> events = new ArrayList<Change>(ticket.changes);
848         Collections.sort(events);
849         Collections.reverse(events);
850         ListDataProvider<Change> eventsDp = new ListDataProvider<Change>(events);
851         DataView<Change> eventsView = new DataView<Change>("event", eventsDp) {
852             private static final long serialVersionUID = 1L;
853
854             @Override
855             public void populateItem(final Item<Change> item) {
856                 Change event = item.getModelObject();
857
858                 addUserAttributions(item, event, 16);
859
860                 if (event.hasPatchset()) {
861                     // patchset
862                     Patchset patchset = event.patchset;
863                     String what;
864                     if (event.isStatusChange() && (Status.New == event.getStatus())) {
865                         what = getString("gb.proposedThisChange");
866                     } else if (patchset.rev == 1) {
867                         what = MessageFormat.format(getString("gb.uploadedPatchsetN"), patchset.number);
868                     } else {
869                         if (patchset.added == 1) {
870                             what = getString("gb.addedOneCommit");
871                         } else {
872                             what = MessageFormat.format(getString("gb.addedNCommits"), patchset.added);
873                         }
874                     }
875                     item.add(new Label("what", what));
876
877                     LinkPanel psr = new LinkPanel("patchsetRevision", null, patchset.number + "-" + patchset.rev,
878                             ComparePage.class, WicketUtils.newRangeParameter(repositoryName, patchset.parent == null ? patchset.base : patchset.parent, patchset.tip), true);
879                     WicketUtils.setHtmlTooltip(psr, patchset.toString());
c2188a 880                     WicketUtils.setCssClass(psr, "aui-lozenge aui-lozenge-subtle");
5e3521 881                     item.add(psr);
JM 882                     String typeCss = getPatchsetTypeCss(patchset.type);
883                     Label typeLabel = new Label("patchsetType", patchset.type.toString());
884                     if (typeCss == null) {
885                         typeLabel.setVisible(false);
886                     } else {
887                         WicketUtils.setCssClass(typeLabel, typeCss);
888                     }
889                     item.add(typeLabel);
890
cd7e4f 891                     Link<Void> deleteLink = createDeletePatchsetLink(repository, patchset);
PM 892                     
893                     if (user.canDeleteRef(repository)) {
894                         item.add(deleteLink.setVisible(patchset.canDelete));
895                     } else {
896                         item.add(deleteLink.setVisible(false));
897                     }
898
5e3521 899                     // show commit diffstat
JM 900                     item.add(new DiffStatPanel("patchsetDiffStat", patchset.insertions, patchset.deletions, patchset.rev > 1));
901                 } else if (event.hasComment()) {
902                     // comment
903                     item.add(new Label("what", getString("gb.commented")));
904                     item.add(new Label("patchsetRevision").setVisible(false));
905                     item.add(new Label("patchsetType").setVisible(false));
cd7e4f 906                     item.add(new Label("deleteRevision").setVisible(false));
5e3521 907                     item.add(new Label("patchsetDiffStat").setVisible(false));
c2188a 908                 } else if (event.hasReference()) {
PM 909                     // reference
910                     switch (event.reference.getSourceType()) {
911                         case Commit: {
912                             final int shaLen = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);
913                             
914                             item.add(new Label("what", getString("gb.referencedByCommit")));
915                             LinkPanel psr = new LinkPanel("patchsetRevision", null, event.reference.toString().substring(0, shaLen),
916                                     CommitPage.class, WicketUtils.newObjectParameter(repositoryName, event.reference.toString()), true);
917                             WicketUtils.setHtmlTooltip(psr, event.reference.toString());
918                             WicketUtils.setCssClass(psr, "ticketReference-commit shortsha1");
919                             item.add(psr);
920                             
921                         } break;
922                         
923                         case Ticket: {
924                             final String text = MessageFormat.format("ticket/{0}", event.reference.ticketId);
925
926                             item.add(new Label("what", getString("gb.referencedByTicket")));
927                             //NOTE: Ideally reference the exact comment using reference.toString,
928                             //        however anchor hash is used and is escaped resulting in broken link
929                             LinkPanel psr = new LinkPanel("patchsetRevision", null,  text,
930                                     TicketsPage.class, WicketUtils.newObjectParameter(repositoryName, event.reference.ticketId.toString()), true);
931                             WicketUtils.setCssClass(psr, "ticketReference-comment");
932                             item.add(psr);
933                         } break;
934                     
935                         default: {
936                             item.add(new Label("what").setVisible(false));
937                             item.add(new Label("patchsetRevision").setVisible(false));
938                         }
939                     }
940                     
941                     item.add(new Label("patchsetType").setVisible(false));
942                     item.add(new Label("deleteRevision").setVisible(false));
943                     item.add(new Label("patchsetDiffStat").setVisible(false));
5e3521 944                 } else if (event.hasReview()) {
JM 945                     // review
946                     String score;
947                     switch (event.review.score) {
948                     case approved:
949                         score = "<span style='color:darkGreen'>" + getScoreDescription(event.review.score) + "</span>";
950                         break;
951                     case vetoed:
952                         score = "<span style='color:darkRed'>" + getScoreDescription(event.review.score) + "</span>";
953                         break;
954                     default:
955                         score = getScoreDescription(event.review.score);
956                     }
957                     item.add(new Label("what", MessageFormat.format(getString("gb.reviewedPatchsetRev"),
958                             event.review.patchset, event.review.rev, score))
959                             .setEscapeModelStrings(false));
960                     item.add(new Label("patchsetRevision").setVisible(false));
961                     item.add(new Label("patchsetType").setVisible(false));
cd7e4f 962                     item.add(new Label("deleteRevision").setVisible(false));
5e3521 963                     item.add(new Label("patchsetDiffStat").setVisible(false));
JM 964                 } else {
965                     // field change
966                     item.add(new Label("patchsetRevision").setVisible(false));
967                     item.add(new Label("patchsetType").setVisible(false));
cd7e4f 968                     item.add(new Label("deleteRevision").setVisible(false));
5e3521 969                     item.add(new Label("patchsetDiffStat").setVisible(false));
JM 970
971                     String what = "";
972                     if (event.isStatusChange()) {
973                     switch (event.getStatus()) {
974                     case New:
975                         if (ticket.isProposal()) {
976                             what = getString("gb.proposedThisChange");
977                         } else {
978                             what = getString("gb.createdThisTicket");
979                         }
980                         break;
981                     default:
982                         break;
983                     }
984                     }
985                     item.add(new Label("what", what).setVisible(what.length() > 0));
986                 }
987
988                 addDateAttributions(item, event);
989
990                 if (event.hasFieldChanges()) {
991                     StringBuilder sb = new StringBuilder();
992                     sb.append("<table class=\"summary\"><tbody>");
993                     for (Map.Entry<Field, String> entry : event.fields.entrySet()) {
994                         String value;
995                         switch (entry.getKey()) {
996                             case body:
997                                 String body = entry.getValue();
998                                 if (event.isStatusChange() && Status.New == event.getStatus() && StringUtils.isEmpty(body)) {
999                                     // ignore initial empty description
1000                                     continue;
1001                                 }
1002                                 // trim body changes
1003                                 if (StringUtils.isEmpty(body)) {
1004                                     value = "<i>" + ESC_NIL + "</i>";
1005                                 } else {
1006                                     value = StringUtils.trimString(body, Constants.LEN_SHORTLOG_REFS);
1007                                 }
1008                                 break;
1009                             case status:
1010                                 // special handling for status
1011                                 Status status = event.getStatus();
fdd82f 1012                                 String css = TicketsUI.getLozengeClass(status, true);
5e3521 1013                                 value = String.format("<span class=\"%1$s\">%2$s</span>", css, status.toString());
JM 1014                                 break;
1015                             default:
1016                                 value = StringUtils.isEmpty(entry.getValue()) ? ("<i>" + ESC_NIL + "</i>") : StringUtils.escapeForHtml(entry.getValue(), false);
1017                                 break;
1018                         }
1019                         sb.append("<tr><th style=\"width:70px;\">");
3d1b42 1020                         try {
JM 1021                             sb.append(getString("gb." + entry.getKey().name()));
1022                         } catch (Exception e) {
1023                             sb.append(entry.getKey().name());
1024                         }
5e3521 1025                         sb.append("</th><td>");
JM 1026                         sb.append(value);
1027                         sb.append("</td></tr>");
1028                     }
1029                     sb.append("</tbody></table>");
a59627 1030                     String safeHtml = app().xssFilter().relaxed(sb.toString());
JM 1031                     item.add(new Label("fields", safeHtml).setEscapeModelStrings(false));
5e3521 1032                 } else {
JM 1033                     item.add(new Label("fields").setVisible(false));
1034                 }
1035             }
1036         };
1037         revisionHistory.add(eventsView);
1038         add(revisionHistory);
1039     }
1040
1041     protected void addUserAttributions(MarkupContainer container, Change entry, int avatarSize) {
1042         UserModel commenter = app().users().getUserModel(entry.author);
1043         if (commenter == null) {
1044             // unknown user
b57b9e 1045             container.add(new AvatarImage("changeAvatar", entry.author,
5e3521 1046                     entry.author, null, avatarSize, false).setVisible(avatarSize > 0));
JM 1047             container.add(new Label("changeAuthor", entry.author.toLowerCase()));
1048         } else {
1049             // known user
b57b9e 1050             container.add(new AvatarImage("changeAvatar", commenter.getDisplayName(),
5e3521 1051                     commenter.emailAddress, avatarSize > 24 ? "gravatar-round" : null,
JM 1052                             avatarSize, true).setVisible(avatarSize > 0));
1053             container.add(new LinkPanel("changeAuthor", null, commenter.getDisplayName(),
1054                     UserPage.class, WicketUtils.newUsernameParameter(commenter.username)));
1055         }
1056     }
1057
1058     protected void addDateAttributions(MarkupContainer container, Change entry) {
1059         container.add(WicketUtils.createDateLabel("changeDate", entry.date, GitBlitWebSession
1060                 .get().getTimezone(), getTimeUtils(), false));
1061
1062         // set the id attribute
1063         if (entry.hasComment()) {
1064             container.setOutputMarkupId(true);
1065             container.add(new AttributeModifier("id", Model.of(entry.getId())));
1066             ExternalLink link = new ExternalLink("changeLink", "#" + entry.getId());
1067             container.add(link);
1068         } else {
1069             container.add(new Label("changeLink").setVisible(false));
1070         }
1071     }
1072
1073     protected String getProposeWorkflow(String resource, String url, long ticketId) {
1074         String md = readResource(resource);
1075         md = md.replace("${url}", url);
1076         md = md.replace("${repo}", StringUtils.getLastPathElement(StringUtils.stripDotGit(repositoryName)));
1077         md = md.replace("${ticketId}", "" + ticketId);
1078         md = md.replace("${patchset}", "" + 1);
1079         md = md.replace("${reviewBranch}", Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticketId)));
f1b882 1080         String integrationBranch = Repository.shortenRefName(getRepositoryModel().mergeTo);
b335dd 1081         if (!StringUtils.isEmpty(ticket.mergeTo)) {
JM 1082             integrationBranch = ticket.mergeTo;
1083         }
1084         md = md.replace("${integrationBranch}", integrationBranch);
5e3521 1085         return MarkdownUtils.transformMarkdown(md);
JM 1086     }
1087
1088     protected Fragment createPatchsetPanel(String wicketId, RepositoryModel repository, UserModel user) {
1089         final Patchset currentPatchset = ticket.getCurrentPatchset();
1090         List<Patchset> patchsets = new ArrayList<Patchset>(ticket.getPatchsetRevisions(currentPatchset.number));
1091         patchsets.remove(currentPatchset);
1092         Collections.reverse(patchsets);
1093
1094         Fragment panel = new Fragment(wicketId, "collapsiblePatchsetFragment", this);
1095
1096         // patchset header
1097         String ps = "<b>" + currentPatchset.number + "</b>";
1098         if (currentPatchset.rev == 1) {
1099             panel.add(new Label("uploadedWhat", MessageFormat.format(getString("gb.uploadedPatchsetN"), ps)).setEscapeModelStrings(false));
1100         } else {
1101             String rev = "<b>" + currentPatchset.rev + "</b>";
1102             panel.add(new Label("uploadedWhat", MessageFormat.format(getString("gb.uploadedPatchsetNRevisionN"), ps, rev)).setEscapeModelStrings(false));
1103         }
1104         panel.add(new LinkPanel("patchId", null, "rev " + currentPatchset.rev,
1105                 CommitPage.class, WicketUtils.newObjectParameter(repositoryName, currentPatchset.tip), true));
1106
1107         // compare menu
1108         panel.add(new LinkPanel("compareMergeBase", null, getString("gb.compareToMergeBase"),
1109                 ComparePage.class, WicketUtils.newRangeParameter(repositoryName, currentPatchset.base, currentPatchset.tip), true));
1110
1111         ListDataProvider<Patchset> compareMenuDp = new ListDataProvider<Patchset>(patchsets);
1112         DataView<Patchset> compareMenu = new DataView<Patchset>("comparePatch", compareMenuDp) {
1113             private static final long serialVersionUID = 1L;
1114             @Override
1115             public void populateItem(final Item<Patchset> item) {
1116                 Patchset patchset = item.getModelObject();
1117                 LinkPanel link = new LinkPanel("compareLink", null,
1118                         MessageFormat.format(getString("gb.compareToN"), patchset.number + "-" + patchset.rev),
1119                         ComparePage.class, WicketUtils.newRangeParameter(getRepositoryModel().name,
1120                                 patchset.tip, currentPatchset.tip), true);
1121                 item.add(link);
1122
1123             }
1124         };
1125         panel.add(compareMenu);
1126
1127
1128         // reviews
1129         List<Change> reviews = ticket.getReviews(currentPatchset);
1130         ListDataProvider<Change> reviewsDp = new ListDataProvider<Change>(reviews);
1131         DataView<Change> reviewsView = new DataView<Change>("reviews", reviewsDp) {
1132             private static final long serialVersionUID = 1L;
1133
1134             @Override
1135             public void populateItem(final Item<Change> item) {
1136                 Change change = item.getModelObject();
1137                 final String username = change.author;
1138                 UserModel user = app().users().getUserModel(username);
1139                 if (user == null) {
1140                     item.add(new Label("reviewer", username));
1141                 } else {
1142                     item.add(new LinkPanel("reviewer", null, user.getDisplayName(),
1143                             UserPage.class, WicketUtils.newUsernameParameter(username)));
1144                 }
1145
1146                 // indicate review score
1147                 Review review = change.review;
1148                 Label scoreLabel = new Label("score");
1149                 String scoreClass = getScoreClass(review.score);
1150                 String tooltip = getScoreDescription(review.score);
1151                 WicketUtils.setCssClass(scoreLabel, scoreClass);
1152                 if (!StringUtils.isEmpty(tooltip)) {
1153                     WicketUtils.setHtmlTooltip(scoreLabel, tooltip);
1154                 }
1155                 item.add(scoreLabel);
1156             }
1157         };
1158         panel.add(reviewsView);
1159
1160
57a71d 1161         if (ticket.isOpen() && user.canReviewPatchset(repository) && app().tickets().isAcceptingTicketUpdates(repository)) {
5e3521 1162             // can only review open tickets
JM 1163             Review myReview = null;
1164             for (Change change : ticket.getReviews(currentPatchset)) {
1165                 if (change.author.equals(user.username)) {
1166                     myReview = change.review;
1167                 }
1168             }
1169
1170             // user can review, add review controls
1171             Fragment reviewControls = new Fragment("reviewControls", "reviewControlsFragment", this);
1172
1173             // show "approve" button if no review OR not current score
1174             if (user.canApprovePatchset(repository) && (myReview == null || Score.approved != myReview.score)) {
1175                 reviewControls.add(createReviewLink("approveLink", Score.approved));
1176             } else {
1177                 reviewControls.add(new Label("approveLink").setVisible(false));
1178             }
1179
1180             // show "looks good" button if no review OR not current score
1181             if (myReview == null || Score.looks_good != myReview.score) {
1182                 reviewControls.add(createReviewLink("looksGoodLink", Score.looks_good));
1183             } else {
1184                 reviewControls.add(new Label("looksGoodLink").setVisible(false));
1185             }
1186
1187             // show "needs improvement" button if no review OR not current score
1188             if (myReview == null || Score.needs_improvement != myReview.score) {
1189                 reviewControls.add(createReviewLink("needsImprovementLink", Score.needs_improvement));
1190             } else {
1191                 reviewControls.add(new Label("needsImprovementLink").setVisible(false));
1192             }
1193
1194             // show "veto" button if no review OR not current score
1195             if (user.canVetoPatchset(repository) && (myReview == null || Score.vetoed != myReview.score)) {
1196                 reviewControls.add(createReviewLink("vetoLink", Score.vetoed));
1197             } else {
1198                 reviewControls.add(new Label("vetoLink").setVisible(false));
1199             }
1200             panel.add(reviewControls);
1201         } else {
1202             // user can not review
1203             panel.add(new Label("reviewControls").setVisible(false));
1204         }
1205
1206         String insertions = MessageFormat.format("<span style=\"color:darkGreen;font-weight:bold;\">+{0}</span>", ticket.insertions);
1207         String deletions = MessageFormat.format("<span style=\"color:darkRed;font-weight:bold;\">-{0}</span>", ticket.deletions);
1208         panel.add(new Label("patchsetStat", MessageFormat.format(StringUtils.escapeForHtml(getString("gb.diffStat"), false),
1209                 insertions, deletions)).setEscapeModelStrings(false));
1210
1211         // changed paths list
1212         List<PathChangeModel> paths = JGitUtils.getFilesInRange(getRepository(), currentPatchset.base, currentPatchset.tip);
1213         ListDataProvider<PathChangeModel> pathsDp = new ListDataProvider<PathChangeModel>(paths);
1214         DataView<PathChangeModel> pathsView = new DataView<PathChangeModel>("changedPath", pathsDp) {
1215             private static final long serialVersionUID = 1L;
1216             int counter;
1217
1218             @Override
1219             public void populateItem(final Item<PathChangeModel> item) {
1220                 final PathChangeModel entry = item.getModelObject();
1221                 Label changeType = new Label("changeType", "");
1222                 WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);
1223                 setChangeTypeTooltip(changeType, entry.changeType);
1224                 item.add(changeType);
1225
1226                 boolean hasSubmodule = false;
1227                 String submodulePath = null;
1228                 if (entry.isTree()) {
1229                     // tree
1230                     item.add(new LinkPanel("pathName", null, entry.path, TreePage.class,
1231                             WicketUtils
1232                                     .newPathParameter(repositoryName, currentPatchset.tip, entry.path), true));
1233                     item.add(new Label("diffStat").setVisible(false));
1234                 } else if (entry.isSubmodule()) {
1235                     // submodule
1236                     String submoduleId = entry.objectId;
1237                     SubmoduleModel submodule = getSubmodule(entry.path);
1238                     submodulePath = submodule.gitblitPath;
1239                     hasSubmodule = submodule.hasSubmodule;
1240
1241                     item.add(new LinkPanel("pathName", "list", entry.path + " @ " +
1242                             getShortObjectId(submoduleId), TreePage.class,
1243                             WicketUtils.newPathParameter(submodulePath, submoduleId, ""), true).setEnabled(hasSubmodule));
1244                     item.add(new Label("diffStat").setVisible(false));
1245                 } else {
1246                     // blob
1247                     String displayPath = entry.path;
1248                     String path = entry.path;
1249                     if (entry.isSymlink()) {
e562d4 1250                         RevCommit commit = JGitUtils.getCommit(getRepository(), PatchsetCommand.getTicketBranch(ticket.number));
5e3521 1251                         path = JGitUtils.getStringContent(getRepository(), commit.getTree(), path);
JM 1252                         displayPath = entry.path + " -> " + path;
1253                     }
1254
1255                     if (entry.changeType.equals(ChangeType.ADD)) {
1256                         // add show view
1257                         item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
1258                                 WicketUtils.newPathParameter(repositoryName, currentPatchset.tip, path), true));
1259                     } else if (entry.changeType.equals(ChangeType.DELETE)) {
1260                         // delete, show label
1261                         item.add(new Label("pathName", displayPath));
1262                     } else {
1263                         // mod, show diff
1264                         item.add(new LinkPanel("pathName", "list", displayPath, BlobDiffPage.class,
1265                                 WicketUtils.newPathParameter(repositoryName, currentPatchset.tip, path), true));
1266                     }
bbd820 1267                     item.add(new DiffStatPanel("diffStat", entry.insertions, entry.deletions, true));
5e3521 1268                 }
JM 1269
1270                 // quick links
1271                 if (entry.isSubmodule()) {
1272                     // submodule
1273                     item.add(setNewTarget(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
1274                             .newPathParameter(repositoryName, entry.commitId, entry.path)))
1275                             .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
1276                     item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
1277                             .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
1278                 } else {
1279                     // tree or blob
1280                     item.add(setNewTarget(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
1281                             .newBlobDiffParameter(repositoryName, currentPatchset.base, currentPatchset.tip, entry.path)))
1282                             .setEnabled(!entry.changeType.equals(ChangeType.ADD)
1283                                     && !entry.changeType.equals(ChangeType.DELETE)));
1284                     item.add(setNewTarget(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
1285                             .newPathParameter(repositoryName, currentPatchset.tip, entry.path)))
1286                             .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
1287                 }
1288
1289                 WicketUtils.setAlternatingBackground(item, counter);
1290                 counter++;
1291             }
1292         };
1293         panel.add(pathsView);
1294
1221a4 1295         addPtCheckoutInstructions(user, repository, panel);
JM 1296         addGitCheckoutInstructions(user, repository, panel);
5e3521 1297
JM 1298         return panel;
1299     }
1300
1301     protected IconAjaxLink<String> createReviewLink(String wicketId, final Score score) {
1302         return new IconAjaxLink<String>(wicketId, getScoreClass(score), Model.of(getScoreDescription(score))) {
1303
1304             private static final long serialVersionUID = 1L;
1305
1306             @Override
1307             public void onClick(AjaxRequestTarget target) {
1308                 review(score);
1309             }
1310         };
1311     }
1312
1313     protected String getScoreClass(Score score) {
1314         switch (score) {
1315         case vetoed:
1316             return "fa fa-exclamation-circle";
1317         case needs_improvement:
1318             return "fa fa-thumbs-o-down";
1319         case looks_good:
1320             return "fa fa-thumbs-o-up";
1321         case approved:
1322             return "fa fa-check-circle";
1323         case not_reviewed:
1324         default:
1325             return "fa fa-minus-circle";
1326         }
1327     }
1328
1329     protected String getScoreDescription(Score score) {
1330         String description;
1331         switch (score) {
1332         case vetoed:
1333             description = getString("gb.veto");
1334             break;
1335         case needs_improvement:
1336             description = getString("gb.needsImprovement");
1337             break;
1338         case looks_good:
1339             description = getString("gb.looksGood");
1340             break;
1341         case approved:
1342             description = getString("gb.approve");
1343             break;
1344         case not_reviewed:
1345         default:
1346             description = getString("gb.hasNotReviewed");
1347         }
1348         return String.format("%1$s (%2$+d)", description, score.getValue());
1349     }
1350
1351     protected void review(Score score) {
1352         UserModel user = GitBlitWebSession.get().getUser();
1353         Patchset ps = ticket.getCurrentPatchset();
1354         Change change = new Change(user.username);
1355         change.review(ps, score, !ticket.isReviewer(user.username));
1356         if (!ticket.isWatching(user.username)) {
1357             change.watch(user.username);
1358         }
1359         TicketModel updatedTicket = app().tickets().updateTicket(getRepositoryModel(), ticket.number, change);
1360         app().tickets().createNotifier().sendMailing(updatedTicket);
47a544 1361         redirectTo(TicketsPage.class, getPageParameters());
5e3521 1362     }
JM 1363
1364     protected <X extends MarkupContainer> X setNewTarget(X x) {
1365         x.add(new SimpleAttributeModifier("target", "_blank"));
1366         return x;
1367     }
1368
1221a4 1369     protected void addGitCheckoutInstructions(UserModel user, RepositoryModel repository, MarkupContainer panel) {
5e3521 1370         panel.add(new Label("gitStep1", MessageFormat.format(getString("gb.stepN"), 1)));
JM 1371         panel.add(new Label("gitStep2", MessageFormat.format(getString("gb.stepN"), 2)));
1372
1373         String ticketBranch  = Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticket.number));
1374
1221a4 1375         String step1 = "git fetch origin";
2f8b4f 1376         String step2 = MessageFormat.format("git checkout {0} && git pull --ff-only\nOR\ngit checkout {0} && git reset --hard origin/{0}", ticketBranch);
5e3521 1377
JM 1378         panel.add(new Label("gitPreStep1", step1));
1379         panel.add(new Label("gitPreStep2", step2));
1380
1381         panel.add(createCopyFragment("gitCopyStep1", step1.replace("\n", " && ")));
1382         panel.add(createCopyFragment("gitCopyStep2", step2.replace("\n", " && ")));
1383     }
1384
1221a4 1385     protected void addPtCheckoutInstructions(UserModel user, RepositoryModel repository, MarkupContainer panel) {
5e3521 1386         String step1 = MessageFormat.format("pt checkout {0,number,0}", ticket.number);
JM 1387         panel.add(new Label("ptPreStep", step1));
1388         panel.add(createCopyFragment("ptCopyStep", step1));
1389     }
1390
1391     /**
1392      * Adds a merge panel for the patchset to the markup container.  The panel
1393      * may just a message if the patchset can not be merged.
1394      *
1395      * @param c
1396      * @param user
1397      * @param repository
1398      */
1399     protected Component createMergePanel(UserModel user, RepositoryModel repository) {
1400         Patchset patchset = ticket.getCurrentPatchset();
1401         if (patchset == null) {
1402             // no patchset to merge
1403             return new Label("mergePanel");
1404         }
1405
1406         boolean allowMerge;
1407         if (repository.requireApproval) {
1408             // rpeository requires approval
1409             allowMerge = ticket.isOpen() && ticket.isApproved(patchset);
1410         } else {
1411             // vetos are binding
1412             allowMerge = ticket.isOpen() && !ticket.isVetoed(patchset);
1413         }
1414
1415         MergeStatus mergeStatus = JGitUtils.canMerge(getRepository(), patchset.tip, ticket.mergeTo);
1416         if (allowMerge) {
1417             if (MergeStatus.MERGEABLE == mergeStatus) {
1418                 // patchset can be cleanly merged to integration branch OR has already been merged
1419                 Fragment mergePanel = new Fragment("mergePanel", "mergeableFragment", this);
1420                 mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetMergeable"), ticket.mergeTo)));
1421                 if (user.canPush(repository)) {
1422                     // user can merge locally
1423                     SimpleAjaxLink<String> mergeButton = new SimpleAjaxLink<String>("mergeButton", Model.of(getString("gb.merge"))) {
1424
1425                         private static final long serialVersionUID = 1L;
1426
1427                         @Override
1428                         public void onClick(AjaxRequestTarget target) {
1429
1430                             // ensure the patchset is still current AND not vetoed
1431                             Patchset patchset = ticket.getCurrentPatchset();
1432                             final TicketModel refreshedTicket = app().tickets().getTicket(getRepositoryModel(), ticket.number);
1433                             if (patchset.equals(refreshedTicket.getCurrentPatchset())) {
1434                                 // patchset is current, check for recent veto
1435                                 if (!refreshedTicket.isVetoed(patchset)) {
1436                                     // patchset is not vetoed
1437
1438                                     // execute the merge using the ticket service
1439                                     app().tickets().exec(new Runnable() {
1440                                         @Override
1441                                         public void run() {
1442                                             PatchsetReceivePack rp = new PatchsetReceivePack(
1443                                                     app().gitblit(),
1444                                                     getRepository(),
1445                                                     getRepositoryModel(),
1446                                                     GitBlitWebSession.get().getUser());
1447                                             MergeStatus result = rp.merge(refreshedTicket);
1448                                             if (MergeStatus.MERGED == result) {
1449                                                 // notify participants and watchers
1450                                                 rp.sendAll();
1451                                             } else {
1452                                                 // merge failure
1453                                                 String msg = MessageFormat.format("Failed to merge ticket {0,number,0}: {1}", ticket.number, result.name());
1454                                                 logger.error(msg);
1455                                                 GitBlitWebSession.get().cacheErrorMessage(msg);
1456                                             }
1457                                         }
1458                                     });
1459                                 } else {
1460                                     // vetoed patchset
1461                                     String msg = MessageFormat.format("Can not merge ticket {0,number,0}, patchset {1,number,0} has been vetoed!",
1462                                             ticket.number, patchset.number);
1463                                     GitBlitWebSession.get().cacheErrorMessage(msg);
1464                                     logger.error(msg);
1465                                 }
1466                             } else {
1467                                 // not current patchset
1468                                 String msg = MessageFormat.format("Can not merge ticket {0,number,0}, the patchset has been updated!", ticket.number);
1469                                 GitBlitWebSession.get().cacheErrorMessage(msg);
1470                                 logger.error(msg);
1471                             }
47a544 1472                             
K 1473                             redirectTo(TicketsPage.class, getPageParameters());
5e3521 1474                         }
JM 1475                     };
1476                     mergePanel.add(mergeButton);
1477                     Component instructions = getMergeInstructions(user, repository, "mergeMore", "gb.patchsetMergeableMore");
1478                     mergePanel.add(instructions);
1479                 } else {
1480                     mergePanel.add(new Label("mergeButton").setVisible(false));
1481                     mergePanel.add(new Label("mergeMore").setVisible(false));
1482                 }
1483                 return mergePanel;
1484             } else if (MergeStatus.ALREADY_MERGED == mergeStatus) {
1485                 // patchset already merged
1486                 Fragment mergePanel = new Fragment("mergePanel", "alreadyMergedFragment", this);
1487                 mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetAlreadyMerged"), ticket.mergeTo)));
1488                 return mergePanel;
4a2fb1 1489             } else if (MergeStatus.MISSING_INTEGRATION_BRANCH == mergeStatus) {
JM 1490                 // target/integration branch is missing
1491                 Fragment mergePanel = new Fragment("mergePanel", "notMergeableFragment", this);
1492                 mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
1493                 mergePanel.add(new Label("mergeMore", MessageFormat.format(getString("gb.missingIntegrationBranchMore"), ticket.mergeTo)));
1494                 return mergePanel;
5e3521 1495             } else {
JM 1496                 // patchset can not be cleanly merged
1497                 Fragment mergePanel = new Fragment("mergePanel", "notMergeableFragment", this);
1498                 mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
1499                 if (user.canPush(repository)) {
1500                     // user can merge locally
1501                     Component instructions = getMergeInstructions(user, repository, "mergeMore", "gb.patchsetNotMergeableMore");
1502                     mergePanel.add(instructions);
1503                 } else {
1504                     mergePanel.add(new Label("mergeMore").setVisible(false));
1505                 }
1506                 return mergePanel;
1507             }
1508         } else {
1509             // merge not allowed
1510             if (MergeStatus.ALREADY_MERGED == mergeStatus) {
1511                 // patchset already merged
1512                 Fragment mergePanel = new Fragment("mergePanel", "alreadyMergedFragment", this);
1513                 mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetAlreadyMerged"), ticket.mergeTo)));
1514                 return mergePanel;
1515             } else if (ticket.isVetoed(patchset)) {
1516                 // patchset has been vetoed
1517                 Fragment mergePanel =  new Fragment("mergePanel", "vetoedFragment", this);
1518                 mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
1519                 return mergePanel;
1520             } else if (repository.requireApproval) {
1521                 // patchset has been not been approved for merge
1522                 Fragment mergePanel = new Fragment("mergePanel", "notApprovedFragment", this);
1523                 mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotApproved"), ticket.mergeTo)));
1524                 mergePanel.add(new Label("mergeMore", MessageFormat.format(getString("gb.patchsetNotApprovedMore"), ticket.mergeTo)));
1525                 return mergePanel;
1526             } else {
1527                 // other case
1528                 return new Label("mergePanel");
1529             }
1530         }
1531     }
1532
1533     protected Component getMergeInstructions(UserModel user, RepositoryModel repository, String markupId, String infoKey) {
1534         Fragment cmd = new Fragment(markupId, "commandlineMergeFragment", this);
1535         cmd.add(new Label("instructions", MessageFormat.format(getString(infoKey), ticket.mergeTo)));
1536
1537         // git instructions
1538         cmd.add(new Label("mergeStep1", MessageFormat.format(getString("gb.stepN"), 1)));
1539         cmd.add(new Label("mergeStep2", MessageFormat.format(getString("gb.stepN"), 2)));
1540         cmd.add(new Label("mergeStep3", MessageFormat.format(getString("gb.stepN"), 3)));
1541
1542         String ticketBranch = Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticket.number));
1543         String reviewBranch = PatchsetCommand.getReviewBranch(ticket.number);
1544
bf426b 1545         String step1 = MessageFormat.format("git checkout -b {0} {1}", reviewBranch, ticket.mergeTo);
JM 1546         String step2 = MessageFormat.format("git pull origin {0}", ticketBranch);
1547         String step3 = MessageFormat.format("git checkout {0}\ngit merge {1}\ngit push origin {0}\ngit branch -d {1}", ticket.mergeTo, reviewBranch);
5e3521 1548
JM 1549         cmd.add(new Label("mergePreStep1", step1));
1550         cmd.add(new Label("mergePreStep2", step2));
1551         cmd.add(new Label("mergePreStep3", step3));
1552
1553         cmd.add(createCopyFragment("mergeCopyStep1", step1.replace("\n", " && ")));
1554         cmd.add(createCopyFragment("mergeCopyStep2", step2.replace("\n", " && ")));
1555         cmd.add(createCopyFragment("mergeCopyStep3", step3.replace("\n", " && ")));
1556
1557         // pt instructions
1558         String ptStep = MessageFormat.format("pt pull {0,number,0}", ticket.number);
1559         cmd.add(new Label("ptMergeStep", ptStep));
1560         cmd.add(createCopyFragment("ptMergeCopyStep", step1.replace("\n", " && ")));
1561         return cmd;
1562     }
1563
1564     /**
1565      * Returns the primary repository url
1566      *
1567      * @param user
1568      * @param repository
1569      * @return the primary repository url
1570      */
d85bfb 1571     protected RepositoryUrl getRepositoryUrl(UserModel user, RepositoryModel repository) {
5e3521 1572         HttpServletRequest req = ((WebRequest) getRequest()).getHttpServletRequest();
7d3a31 1573         List<RepositoryUrl> urls = app().services().getRepositoryUrls(req, user, repository);
b236ce 1574         if (ArrayUtils.isEmpty(urls)) {
JM 1575             return null;
1576         }
d85bfb 1577         RepositoryUrl primary = urls.get(0);
JM 1578         return primary;
5e3521 1579     }
JM 1580
1581     /**
1582      * Returns the ticket (if any) that this commit references.
1583      *
1584      * @param commit
1585      * @return null or a ticket
1586      */
1587     protected TicketModel getTicket(RevCommit commit) {
1588         try {
1589             Map<String, Ref> refs = getRepository().getRefDatabase().getRefs(Constants.R_TICKETS_PATCHSETS);
1590             for (Map.Entry<String, Ref> entry : refs.entrySet()) {
1591                 if (entry.getValue().getObjectId().equals(commit.getId())) {
1592                     long id = PatchsetCommand.getTicketNumber(entry.getKey());
1593                     TicketModel ticket = app().tickets().getTicket(getRepositoryModel(), id);
1594                     return ticket;
1595                 }
1596             }
1597         } catch (Exception e) {
1598             logger().error("failed to determine ticket from ref", e);
1599         }
1600         return null;
1601     }
1602
1603     protected String getPatchsetTypeCss(PatchsetType type) {
1604         String typeCss;
1605         switch (type) {
1606             case Rebase:
1607             case Rebase_Squash:
fdd82f 1608                 typeCss = TicketsUI.getLozengeClass(Status.Declined, false);
5e3521 1609                 break;
JM 1610             case Squash:
1611             case Amend:
fdd82f 1612                 typeCss = TicketsUI.getLozengeClass(Status.On_Hold, false);
5e3521 1613                 break;
JM 1614             case Proposal:
fdd82f 1615                 typeCss = TicketsUI.getLozengeClass(Status.New, false);
5e3521 1616                 break;
JM 1617             case FastForward:
1618             default:
1619                 typeCss = null;
1620             break;
1621         }
1622         return typeCss;
1623     }
1624
1625     @Override
1626     protected String getPageName() {
1627         return getString("gb.ticket");
1628     }
1629
1630     @Override
1631     protected Class<? extends BasePage> getRepoNavPageClass() {
1632         return TicketsPage.class;
1633     }
1634
1635     @Override
1636     protected String getPageTitle(String repositoryName) {
1637         return "#" + ticket.number + " - " + ticket.title;
1638     }
1639
1640     protected Fragment createCopyFragment(String wicketId, String text) {
1641         if (app().settings().getBoolean(Keys.web.allowFlashCopyToClipboard, true)) {
1642             // clippy: flash-based copy & paste
1643             Fragment copyFragment = new Fragment(wicketId, "clippyPanel", this);
1644             String baseUrl = WicketUtils.getGitblitURL(getRequest());
1645             ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf");
1646             clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(text));
1647             copyFragment.add(clippy);
1648             return copyFragment;
1649         } else {
1650             // javascript: manual copy & paste with modal browser prompt dialog
1651             Fragment copyFragment = new Fragment(wicketId, "jsPanel", this);
1652             ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png");
1653             img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", text));
1654             copyFragment.add(img);
1655             return copyFragment;
1656         }
1657     }
cd7e4f 1658     
PM 1659     private Link<Void> createDeletePatchsetLink(final RepositoryModel repositoryModel, final Patchset patchset)
1660     {
1661         Link<Void> deleteLink = new Link<Void>("deleteRevision") {
1662             private static final long serialVersionUID = 1L;
1663
1664             @Override
1665             public void onClick() {
1666                 Repository r = app().repositories().getRepository(repositoryModel.name);
1667                 UserModel user = GitBlitWebSession.get().getUser();
1668                 
1669                 if (r == null) {
1670                     if (app().repositories().isCollectingGarbage(repositoryModel.name)) {
1671                         error(MessageFormat.format(getString("gb.busyCollectingGarbage"), repositoryModel.name));
1672                     } else {
1673                         error(MessageFormat.format("Failed to find repository {0}", repositoryModel.name));
1674                     }
1675                     return;
1676                 }
1677                 
1678                 //Construct the ref name based on the patchset
1679                 String ticketShard = String.format("%02d", ticket.number);
1680                 ticketShard = ticketShard.substring(ticketShard.length() - 2);
1681                 final String refName = String.format("%s%s/%d/%d", Constants.R_TICKETS_PATCHSETS, ticketShard, ticket.number, patchset.number);
1682
1683                 Ref ref = null;
1684                 boolean success = true;
1685                 
1686                 try {
1687                     ref = r.getRef(refName);
1688                     
1689                     if (ref != null) {
1690                         success = JGitUtils.deleteBranchRef(r, ref.getName());
1691                     } else {
1692                         success = false;
1693                     }
1694                     
1695                     if (success) {
1696                         // clear commit cache
1697                         CommitCache.instance().clear(repositoryModel.name, refName);
1698
1699                         // optionally update reflog
1700                         if (RefLogUtils.hasRefLogBranch(r)) {
1701                             RefLogUtils.deleteRef(user, r, ref);
1702                         }
1703
1704                         TicketModel updatedTicket = app().tickets().deletePatchset(ticket, patchset, user.username);
1705                                                 
1706                         if (updatedTicket == null) {
1707                             success = false;
1708                         }
1709                     }
1710                 } catch (IOException e) {
1711                     logger().error("failed to determine ticket from ref", e);
1712                     success = false;
1713                 } finally {
1714                     r.close();
1715                 }
1716
1717                 if (success) {
1718                     getSession().info(MessageFormat.format(getString("gb.deletePatchsetSuccess"), patchset.number));
1719                     logger().info(MessageFormat.format("{0} deleted patchset {1} from ticket {2}", 
1720                             user.username, patchset.number, ticket.number));
1721                 } else {
1722                     getSession().error(MessageFormat.format(getString("gb.deletePatchsetFailure"),patchset.number));
1723                 }
1724                 
1725                 //Force reload of the page to rebuild ticket change cache
1726                 String relativeUrl = urlFor(TicketsPage.class, getPageParameters()).toString();
1727                 String absoluteUrl = RequestUtils.toAbsolutePath(relativeUrl);
1728                 setResponsePage(new RedirectPage(absoluteUrl));
1729             }
1730         };
1731
1732         WicketUtils.setHtmlTooltip(deleteLink, MessageFormat.format(getString("gb.deletePatchset"), patchset.number));
1733         
1734         deleteLink.add(new JavascriptEventConfirmation("onclick", MessageFormat.format(getString("gb.deletePatchset"), patchset.number)));
1735         
1736         return deleteLink;
1737     }
1738     
5e3521 1739 }