You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Ticket tracker with patchset contributions A basic issue tracker styled as a hybrid of GitHub and BitBucket issues. You may attach commits to an existing ticket or you can push a single commit to create a *proposal* ticket. Tickets keep track of patchsets (one or more commits) and allow patchset rewriting (rebase, amend, squash) by detecing the non-fast-forward update and assigning a new patchset number to the new commits. Ticket tracker -------------- The ticket tracker stores tickets as an append-only journal of changes. The journals are deserialized and a ticket is built by applying the journal entries. Tickets are indexed using Apache Lucene and all queries and searches are executed against this Lucene index. There is one trade-off to this persistence design: user attributions are non-relational. What does that mean? Each journal entry stores the username of the author. If the username changes in the user service, the journal entry will not reflect that change because the values are hard-coded. Here are a few reasons/justifications for this design choice: 1. commit identifications (author, committer, tagger) are non-relational 2. maintains the KISS principle 3. your favorite text editor can still be your administration tool Persistence Choices ------------------- **FileTicketService**: stores journals on the filesystem **BranchTicketService**: stores journals on an orphan branch **RedisTicketService**: stores journals in a Redis key-value datastore It should be relatively straight-forward to develop other backends (MongoDB, etc) as long as the journal design is preserved. Pushing Commits --------------- Each push to a ticket is identified as a patchset revision. A patchset revision may add commits to the patchset (fast-forward) OR a patchset revision may rewrite history (rebase, squash, rebase+squash, or amend). Patchset authors should not be afraid to polish, revise, and rewrite their code before merging into the proposed branch. Gitblit will create one ref for each patchset. These refs are updated for fast-forward pushes or created for rewrites. They are formatted as `refs/tickets/{shard}/{id}/{patchset}`. The *shard* is the last two digits of the id. If the id < 10, prefix a 0. The *shard* is always two digits long. The shard's purpose is to ensure Gitblit doesn't exceed any filesystem directory limits for file creation. **Creating a Proposal Ticket** You may create a new change proposal ticket just by pushing a **single commit** to `refs/for/{branch}` where branch is the proposed integration branch OR `refs/for/new` or `refs/for/default` which both will use the default repository branch. git push origin HEAD:refs/for/new **Updating a Patchset** The safe way to update an existing patchset is to push to the patchset ref. git push origin HEAD:refs/heads/ticket/{id} This ensures you do not accidentally create a new patchset in the event that the patchset was updated after you last pulled. The not-so-safe way to update an existing patchset is to push using the magic ref. git push origin HEAD:refs/for/{id} This push ref will update an exisitng patchset OR create a new patchset if the update is non-fast-forward. **Rebasing, Squashing, Amending** Gitblit makes rebasing, squashing, and amending patchsets easy. Normally, pushing a non-fast-forward update would require rewind (RW+) repository permissions. Gitblit provides a magic ref which will allow ticket participants to rewrite a ticket patchset as long as the ticket is open. git push origin HEAD:refs/for/{id} Pushing changes to this ref allows the patchset authors to rebase, squash, or amend the patchset commits without requiring client-side use of the *--force* flag on push AND without requiring RW+ permission to the repository. Since each patchset is tracked with a ref it is easy to recover from accidental non-fast-forward updates. Features -------- - Ticket tracker with status changes and responsible assignments - Patchset revision scoring mechanism - Update/Rewrite patchset handling - Close-on-push detection - Server-side Merge button for simple merges - Comments with Markdown syntax support - Rich mail notifications - Voting - Mentions - Watch lists - Querying - Searches - Partial miletones support - Multiple backend options
преди 10 години
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527
  1. /*
  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. import java.text.DateFormat;
  18. import java.text.MessageFormat;
  19. import java.text.SimpleDateFormat;
  20. import java.util.ArrayList;
  21. import java.util.Arrays;
  22. import java.util.Calendar;
  23. import java.util.Collections;
  24. import java.util.Date;
  25. import java.util.List;
  26. import java.util.Map;
  27. import java.util.Set;
  28. import java.util.TimeZone;
  29. import java.util.TreeSet;
  30. import javax.servlet.http.HttpServletRequest;
  31. import org.apache.wicket.AttributeModifier;
  32. import org.apache.wicket.Component;
  33. import org.apache.wicket.MarkupContainer;
  34. import org.apache.wicket.PageParameters;
  35. import org.apache.wicket.RestartResponseException;
  36. import org.apache.wicket.ajax.AjaxRequestTarget;
  37. import org.apache.wicket.behavior.IBehavior;
  38. import org.apache.wicket.behavior.SimpleAttributeModifier;
  39. import org.apache.wicket.markup.html.basic.Label;
  40. import org.apache.wicket.markup.html.image.ContextImage;
  41. import org.apache.wicket.markup.html.link.BookmarkablePageLink;
  42. import org.apache.wicket.markup.html.link.ExternalLink;
  43. import org.apache.wicket.markup.html.panel.Fragment;
  44. import org.apache.wicket.markup.repeater.Item;
  45. import org.apache.wicket.markup.repeater.data.DataView;
  46. import org.apache.wicket.markup.repeater.data.ListDataProvider;
  47. import org.apache.wicket.model.Model;
  48. import org.apache.wicket.protocol.http.WebRequest;
  49. import org.eclipse.jgit.diff.DiffEntry.ChangeType;
  50. import org.eclipse.jgit.lib.PersonIdent;
  51. import org.eclipse.jgit.lib.Ref;
  52. import org.eclipse.jgit.lib.Repository;
  53. import org.eclipse.jgit.revwalk.RevCommit;
  54. import org.eclipse.jgit.transport.URIish;
  55. import com.gitblit.Constants;
  56. import com.gitblit.Constants.AccessPermission;
  57. import com.gitblit.Keys;
  58. import com.gitblit.git.PatchsetCommand;
  59. import com.gitblit.git.PatchsetReceivePack;
  60. import com.gitblit.models.PathModel.PathChangeModel;
  61. import com.gitblit.models.RegistrantAccessPermission;
  62. import com.gitblit.models.RepositoryModel;
  63. import com.gitblit.models.SubmoduleModel;
  64. import com.gitblit.models.TicketModel;
  65. import com.gitblit.models.TicketModel.Change;
  66. import com.gitblit.models.TicketModel.CommentSource;
  67. import com.gitblit.models.TicketModel.Field;
  68. import com.gitblit.models.TicketModel.Patchset;
  69. import com.gitblit.models.TicketModel.PatchsetType;
  70. import com.gitblit.models.TicketModel.Review;
  71. import com.gitblit.models.TicketModel.Score;
  72. import com.gitblit.models.TicketModel.Status;
  73. import com.gitblit.models.UserModel;
  74. import com.gitblit.tickets.TicketIndexer.Lucene;
  75. import com.gitblit.tickets.TicketLabel;
  76. import com.gitblit.tickets.TicketMilestone;
  77. import com.gitblit.tickets.TicketResponsible;
  78. import com.gitblit.utils.JGitUtils;
  79. import com.gitblit.utils.JGitUtils.MergeStatus;
  80. import com.gitblit.utils.MarkdownUtils;
  81. import com.gitblit.utils.StringUtils;
  82. import com.gitblit.utils.TimeUtils;
  83. import com.gitblit.wicket.GitBlitWebSession;
  84. import com.gitblit.wicket.WicketUtils;
  85. import com.gitblit.wicket.panels.BasePanel.JavascriptTextPrompt;
  86. import com.gitblit.wicket.panels.CommentPanel;
  87. import com.gitblit.wicket.panels.DiffStatPanel;
  88. import com.gitblit.wicket.panels.GravatarImage;
  89. import com.gitblit.wicket.panels.IconAjaxLink;
  90. import com.gitblit.wicket.panels.LinkPanel;
  91. import com.gitblit.wicket.panels.ShockWaveComponent;
  92. import com.gitblit.wicket.panels.SimpleAjaxLink;
  93. /**
  94. * The ticket page handles viewing and updating a ticket.
  95. *
  96. * @author James Moger
  97. *
  98. */
  99. public class TicketPage extends TicketBasePage {
  100. static final String NIL = "<nil>";
  101. static final String ESC_NIL = StringUtils.escapeForHtml(NIL, false);
  102. final int avatarWidth = 40;
  103. final TicketModel ticket;
  104. public TicketPage(PageParameters params) {
  105. super(params);
  106. final UserModel user = GitBlitWebSession.get().getUser() == null ? UserModel.ANONYMOUS : GitBlitWebSession.get().getUser();
  107. final boolean isAuthenticated = !UserModel.ANONYMOUS.equals(user) && user.isAuthenticated;
  108. final RepositoryModel repository = getRepositoryModel();
  109. final String id = WicketUtils.getObject(params);
  110. long ticketId = Long.parseLong(id);
  111. ticket = app().tickets().getTicket(repository, ticketId);
  112. if (ticket == null) {
  113. // ticket not found
  114. throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName));
  115. }
  116. final List<Change> revisions = new ArrayList<Change>();
  117. List<Change> comments = new ArrayList<Change>();
  118. List<Change> statusChanges = new ArrayList<Change>();
  119. List<Change> discussion = new ArrayList<Change>();
  120. for (Change change : ticket.changes) {
  121. if (change.hasComment() || (change.isStatusChange() && (change.getStatus() != Status.New))) {
  122. discussion.add(change);
  123. }
  124. if (change.hasComment()) {
  125. comments.add(change);
  126. }
  127. if (change.hasPatchset()) {
  128. revisions.add(change);
  129. }
  130. if (change.isStatusChange() && !change.hasPatchset()) {
  131. statusChanges.add(change);
  132. }
  133. }
  134. final Change currentRevision = revisions.isEmpty() ? null : revisions.get(revisions.size() - 1);
  135. final Patchset currentPatchset = ticket.getCurrentPatchset();
  136. /*
  137. * TICKET HEADER
  138. */
  139. String href = urlFor(TicketsPage.class, params).toString();
  140. add(new ExternalLink("ticketNumber", href, "#" + ticket.number));
  141. Label headerStatus = new Label("headerStatus", ticket.status.toString());
  142. WicketUtils.setCssClass(headerStatus, getLozengeClass(ticket.status, false));
  143. add(headerStatus);
  144. add(new Label("ticketTitle", ticket.title));
  145. if (currentPatchset == null) {
  146. add(new Label("diffstat").setVisible(false));
  147. } else {
  148. // calculate the current diffstat of the patchset
  149. add(new DiffStatPanel("diffstat", ticket.insertions, ticket.deletions));
  150. }
  151. /*
  152. * TAB TITLES
  153. */
  154. add(new Label("commentCount", "" + comments.size()).setVisible(!comments.isEmpty()));
  155. add(new Label("commitCount", "" + (currentPatchset == null ? 0 : currentPatchset.commits)).setVisible(currentPatchset != null));
  156. /*
  157. * TICKET AUTHOR and DATE (DISCUSSION TAB)
  158. */
  159. UserModel createdBy = app().users().getUserModel(ticket.createdBy);
  160. if (createdBy == null) {
  161. add(new Label("whoCreated", ticket.createdBy));
  162. } else {
  163. add(new LinkPanel("whoCreated", null, createdBy.getDisplayName(),
  164. UserPage.class, WicketUtils.newUsernameParameter(createdBy.username)));
  165. }
  166. if (ticket.isProposal()) {
  167. // clearly indicate this is a change ticket
  168. add(new Label("creationMessage", getString("gb.proposedThisChange")));
  169. } else {
  170. // standard ticket
  171. add(new Label("creationMessage", getString("gb.createdThisTicket")));
  172. }
  173. String dateFormat = app().settings().getString(Keys.web.datestampLongFormat, "EEEE, MMMM d, yyyy");
  174. String timestampFormat = app().settings().getString(Keys.web.datetimestampLongFormat, "EEEE, MMMM d, yyyy");
  175. final TimeZone timezone = getTimeZone();
  176. final DateFormat df = new SimpleDateFormat(dateFormat);
  177. df.setTimeZone(timezone);
  178. final DateFormat tsf = new SimpleDateFormat(timestampFormat);
  179. tsf.setTimeZone(timezone);
  180. final Calendar cal = Calendar.getInstance(timezone);
  181. String fuzzydate;
  182. TimeUtils tu = getTimeUtils();
  183. Date createdDate = ticket.created;
  184. if (TimeUtils.isToday(createdDate, timezone)) {
  185. fuzzydate = tu.today();
  186. } else if (TimeUtils.isYesterday(createdDate, timezone)) {
  187. fuzzydate = tu.yesterday();
  188. } else {
  189. // calculate a fuzzy time ago date
  190. cal.setTime(createdDate);
  191. cal.set(Calendar.HOUR_OF_DAY, 0);
  192. cal.set(Calendar.MINUTE, 0);
  193. cal.set(Calendar.SECOND, 0);
  194. cal.set(Calendar.MILLISECOND, 0);
  195. createdDate = cal.getTime();
  196. fuzzydate = getTimeUtils().timeAgo(createdDate);
  197. }
  198. Label when = new Label("whenCreated", fuzzydate + ", " + df.format(createdDate));
  199. WicketUtils.setHtmlTooltip(when, tsf.format(ticket.created));
  200. add(when);
  201. String exportHref = urlFor(ExportTicketPage.class, params).toString();
  202. add(new ExternalLink("exportJson", exportHref, "json"));
  203. /*
  204. * RESPONSIBLE (DISCUSSION TAB)
  205. */
  206. if (StringUtils.isEmpty(ticket.responsible)) {
  207. add(new Label("responsible"));
  208. } else {
  209. UserModel responsible = app().users().getUserModel(ticket.responsible);
  210. if (responsible == null) {
  211. add(new Label("responsible", ticket.responsible));
  212. } else {
  213. add(new LinkPanel("responsible", null, responsible.getDisplayName(),
  214. UserPage.class, WicketUtils.newUsernameParameter(responsible.username)));
  215. }
  216. }
  217. /*
  218. * MILESTONE PROGRESS (DISCUSSION TAB)
  219. */
  220. if (StringUtils.isEmpty(ticket.milestone)) {
  221. add(new Label("milestone"));
  222. } else {
  223. // link to milestone query
  224. TicketMilestone milestone = app().tickets().getMilestone(repository, ticket.milestone);
  225. PageParameters milestoneParameters = new PageParameters();
  226. milestoneParameters.put("r", repositoryName);
  227. milestoneParameters.put(Lucene.milestone.name(), ticket.milestone);
  228. int progress = 0;
  229. int open = 0;
  230. int closed = 0;
  231. if (milestone != null) {
  232. progress = milestone.getProgress();
  233. open = milestone.getOpenTickets();
  234. closed = milestone.getClosedTickets();
  235. }
  236. Fragment milestoneProgress = new Fragment("milestone", "milestoneProgressFragment", this);
  237. milestoneProgress.add(new LinkPanel("link", null, ticket.milestone, TicketsPage.class, milestoneParameters));
  238. Label label = new Label("progress");
  239. WicketUtils.setCssStyle(label, "width:" + progress + "%;");
  240. milestoneProgress.add(label);
  241. WicketUtils.setHtmlTooltip(milestoneProgress, MessageFormat.format("{0} open, {1} closed", open, closed));
  242. add(milestoneProgress);
  243. }
  244. /*
  245. * TICKET DESCRIPTION (DISCUSSION TAB)
  246. */
  247. String desc;
  248. if (StringUtils.isEmpty(ticket.body)) {
  249. desc = getString("gb.noDescriptionGiven");
  250. } else {
  251. desc = MarkdownUtils.transformGFM(app().settings(), ticket.body, ticket.repository);
  252. }
  253. add(new Label("ticketDescription", desc).setEscapeModelStrings(false));
  254. /*
  255. * PARTICIPANTS (DISCUSSION TAB)
  256. */
  257. if (app().settings().getBoolean(Keys.web.allowGravatar, true)) {
  258. // gravatar allowed
  259. List<String> participants = ticket.getParticipants();
  260. add(new Label("participantsLabel", MessageFormat.format(getString(participants.size() > 1 ? "gb.nParticipants" : "gb.oneParticipant"),
  261. "<b>" + participants.size() + "</b>")).setEscapeModelStrings(false));
  262. ListDataProvider<String> participantsDp = new ListDataProvider<String>(participants);
  263. DataView<String> participantsView = new DataView<String>("participants", participantsDp) {
  264. private static final long serialVersionUID = 1L;
  265. @Override
  266. public void populateItem(final Item<String> item) {
  267. String username = item.getModelObject();
  268. UserModel user = app().users().getUserModel(username);
  269. if (user == null) {
  270. user = new UserModel(username);
  271. }
  272. item.add(new GravatarImage("participant", user.getDisplayName(),
  273. user.emailAddress, null, 25, true));
  274. }
  275. };
  276. add(participantsView);
  277. } else {
  278. // gravatar prohibited
  279. add(new Label("participantsLabel").setVisible(false));
  280. add(new Label("participants").setVisible(false));
  281. }
  282. /*
  283. * LARGE STATUS INDICATOR WITH ICON (DISCUSSION TAB->SIDE BAR)
  284. */
  285. Fragment ticketStatus = new Fragment("ticketStatus", "ticketStatusFragment", this);
  286. Label ticketIcon = getStateIcon("ticketIcon", ticket);
  287. ticketStatus.add(ticketIcon);
  288. ticketStatus.add(new Label("ticketStatus", ticket.status.toString()));
  289. WicketUtils.setCssClass(ticketStatus, getLozengeClass(ticket.status, false));
  290. add(ticketStatus);
  291. /*
  292. * UPDATE FORM (DISCUSSION TAB)
  293. */
  294. if (isAuthenticated && app().tickets().isAcceptingTicketUpdates(repository)) {
  295. Fragment controls = new Fragment("controls", "controlsFragment", this);
  296. /*
  297. * STATUS
  298. */
  299. List<Status> choices = new ArrayList<Status>();
  300. if (ticket.isProposal()) {
  301. choices.addAll(Arrays.asList(TicketModel.Status.proposalWorkflow));
  302. } else if (ticket.isBug()) {
  303. choices.addAll(Arrays.asList(TicketModel.Status.bugWorkflow));
  304. } else {
  305. choices.addAll(Arrays.asList(TicketModel.Status.requestWorkflow));
  306. }
  307. choices.remove(ticket.status);
  308. ListDataProvider<Status> workflowDp = new ListDataProvider<Status>(choices);
  309. DataView<Status> statusView = new DataView<Status>("newStatus", workflowDp) {
  310. private static final long serialVersionUID = 1L;
  311. @Override
  312. public void populateItem(final Item<Status> item) {
  313. SimpleAjaxLink<Status> link = new SimpleAjaxLink<Status>("link", item.getModel()) {
  314. private static final long serialVersionUID = 1L;
  315. @Override
  316. public void onClick(AjaxRequestTarget target) {
  317. Status status = getModel().getObject();
  318. Change change = new Change(user.username);
  319. change.setField(Field.status, status);
  320. if (!ticket.isWatching(user.username)) {
  321. change.watch(user.username);
  322. }
  323. TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
  324. app().tickets().createNotifier().sendMailing(update);
  325. setResponsePage(TicketsPage.class, getPageParameters());
  326. }
  327. };
  328. String css = getStatusClass(item.getModel().getObject());
  329. WicketUtils.setCssClass(link, css);
  330. item.add(link);
  331. }
  332. };
  333. controls.add(statusView);
  334. /*
  335. * RESPONSIBLE LIST
  336. */
  337. Set<String> userlist = new TreeSet<String>(ticket.getParticipants());
  338. for (RegistrantAccessPermission rp : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
  339. if (rp.permission.atLeast(AccessPermission.PUSH) && !rp.isTeam()) {
  340. userlist.add(rp.registrant);
  341. }
  342. }
  343. List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
  344. if (!StringUtils.isEmpty(ticket.responsible)) {
  345. // exclude the current responsible
  346. userlist.remove(ticket.responsible);
  347. }
  348. for (String username : userlist) {
  349. UserModel u = app().users().getUserModel(username);
  350. if (u != null) {
  351. responsibles.add(new TicketResponsible(u));
  352. }
  353. }
  354. Collections.sort(responsibles);
  355. responsibles.add(new TicketResponsible(ESC_NIL, "", ""));
  356. ListDataProvider<TicketResponsible> responsibleDp = new ListDataProvider<TicketResponsible>(responsibles);
  357. DataView<TicketResponsible> responsibleView = new DataView<TicketResponsible>("newResponsible", responsibleDp) {
  358. private static final long serialVersionUID = 1L;
  359. @Override
  360. public void populateItem(final Item<TicketResponsible> item) {
  361. SimpleAjaxLink<TicketResponsible> link = new SimpleAjaxLink<TicketResponsible>("link", item.getModel()) {
  362. private static final long serialVersionUID = 1L;
  363. @Override
  364. public void onClick(AjaxRequestTarget target) {
  365. TicketResponsible responsible = getModel().getObject();
  366. Change change = new Change(user.username);
  367. change.setField(Field.responsible, responsible.username);
  368. if (!StringUtils.isEmpty(responsible.username)) {
  369. if (!ticket.isWatching(responsible.username)) {
  370. change.watch(responsible.username);
  371. }
  372. }
  373. if (!ticket.isWatching(user.username)) {
  374. change.watch(user.username);
  375. }
  376. TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
  377. app().tickets().createNotifier().sendMailing(update);
  378. setResponsePage(TicketsPage.class, getPageParameters());
  379. }
  380. };
  381. item.add(link);
  382. }
  383. };
  384. controls.add(responsibleView);
  385. /*
  386. * MILESTONE LIST
  387. */
  388. List<TicketMilestone> milestones = app().tickets().getMilestones(repository, Status.Open);
  389. if (!StringUtils.isEmpty(ticket.milestone)) {
  390. for (TicketMilestone milestone : milestones) {
  391. if (milestone.name.equals(ticket.milestone)) {
  392. milestones.remove(milestone);
  393. break;
  394. }
  395. }
  396. }
  397. milestones.add(new TicketMilestone(ESC_NIL));
  398. ListDataProvider<TicketMilestone> milestoneDp = new ListDataProvider<TicketMilestone>(milestones);
  399. DataView<TicketMilestone> milestoneView = new DataView<TicketMilestone>("newMilestone", milestoneDp) {
  400. private static final long serialVersionUID = 1L;
  401. @Override
  402. public void populateItem(final Item<TicketMilestone> item) {
  403. SimpleAjaxLink<TicketMilestone> link = new SimpleAjaxLink<TicketMilestone>("link", item.getModel()) {
  404. private static final long serialVersionUID = 1L;
  405. @Override
  406. public void onClick(AjaxRequestTarget target) {
  407. TicketMilestone milestone = getModel().getObject();
  408. Change change = new Change(user.username);
  409. if (NIL.equals(milestone.name)) {
  410. change.setField(Field.milestone, "");
  411. } else {
  412. change.setField(Field.milestone, milestone.name);
  413. }
  414. if (!ticket.isWatching(user.username)) {
  415. change.watch(user.username);
  416. }
  417. TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
  418. app().tickets().createNotifier().sendMailing(update);
  419. setResponsePage(TicketsPage.class, getPageParameters());
  420. }
  421. };
  422. item.add(link);
  423. }
  424. };
  425. controls.add(milestoneView);
  426. String editHref = urlFor(EditTicketPage.class, params).toString();
  427. controls.add(new ExternalLink("editLink", editHref, getString("gb.edit")));
  428. add(controls);
  429. } else {
  430. add(new Label("controls").setVisible(false));
  431. }
  432. /*
  433. * TICKET METADATA
  434. */
  435. add(new Label("ticketType", ticket.type.toString()));
  436. if (StringUtils.isEmpty(ticket.topic)) {
  437. add(new Label("ticketTopic").setVisible(false));
  438. } else {
  439. // process the topic using the bugtraq config to link things
  440. String topic = messageProcessor().processPlainCommitMessage(getRepository(), repositoryName, ticket.topic);
  441. add(new Label("ticketTopic", topic).setEscapeModelStrings(false));
  442. }
  443. /*
  444. * VOTERS
  445. */
  446. List<String> voters = ticket.getVoters();
  447. Label votersCount = new Label("votes", "" + voters.size());
  448. if (voters.size() == 0) {
  449. WicketUtils.setCssClass(votersCount, "badge");
  450. } else {
  451. WicketUtils.setCssClass(votersCount, "badge badge-info");
  452. }
  453. add(votersCount);
  454. if (user.isAuthenticated) {
  455. Model<String> model;
  456. if (ticket.isVoter(user.username)) {
  457. model = Model.of(getString("gb.removeVote"));
  458. } else {
  459. model = Model.of(MessageFormat.format(getString("gb.vote"), ticket.type.toString()));
  460. }
  461. SimpleAjaxLink<String> link = new SimpleAjaxLink<String>("voteLink", model) {
  462. private static final long serialVersionUID = 1L;
  463. @Override
  464. public void onClick(AjaxRequestTarget target) {
  465. Change change = new Change(user.username);
  466. if (ticket.isVoter(user.username)) {
  467. change.unvote(user.username);
  468. } else {
  469. change.vote(user.username);
  470. }
  471. app().tickets().updateTicket(repository, ticket.number, change);
  472. setResponsePage(TicketsPage.class, getPageParameters());
  473. }
  474. };
  475. add(link);
  476. } else {
  477. add(new Label("voteLink").setVisible(false));
  478. }
  479. /*
  480. * WATCHERS
  481. */
  482. List<String> watchers = ticket.getWatchers();
  483. Label watchersCount = new Label("watchers", "" + watchers.size());
  484. if (watchers.size() == 0) {
  485. WicketUtils.setCssClass(watchersCount, "badge");
  486. } else {
  487. WicketUtils.setCssClass(watchersCount, "badge badge-info");
  488. }
  489. add(watchersCount);
  490. if (user.isAuthenticated) {
  491. Model<String> model;
  492. if (ticket.isWatching(user.username)) {
  493. model = Model.of(getString("gb.stopWatching"));
  494. } else {
  495. model = Model.of(MessageFormat.format(getString("gb.watch"), ticket.type.toString()));
  496. }
  497. SimpleAjaxLink<String> link = new SimpleAjaxLink<String>("watchLink", model) {
  498. private static final long serialVersionUID = 1L;
  499. @Override
  500. public void onClick(AjaxRequestTarget target) {
  501. Change change = new Change(user.username);
  502. if (ticket.isWatching(user.username)) {
  503. change.unwatch(user.username);
  504. } else {
  505. change.watch(user.username);
  506. }
  507. app().tickets().updateTicket(repository, ticket.number, change);
  508. setResponsePage(TicketsPage.class, getPageParameters());
  509. }
  510. };
  511. add(link);
  512. } else {
  513. add(new Label("watchLink").setVisible(false));
  514. }
  515. /*
  516. * TOPIC & LABELS (DISCUSSION TAB->SIDE BAR)
  517. */
  518. ListDataProvider<String> labelsDp = new ListDataProvider<String>(ticket.getLabels());
  519. DataView<String> labelsView = new DataView<String>("labels", labelsDp) {
  520. private static final long serialVersionUID = 1L;
  521. @Override
  522. public void populateItem(final Item<String> item) {
  523. final String value = item.getModelObject();
  524. Label label = new Label("label", value);
  525. TicketLabel tLabel = app().tickets().getLabel(repository, value);
  526. String background = MessageFormat.format("background-color:{0};", tLabel.color);
  527. label.add(new SimpleAttributeModifier("style", background));
  528. item.add(label);
  529. }
  530. };
  531. add(labelsView);
  532. /*
  533. * COMMENTS & STATUS CHANGES (DISCUSSION TAB)
  534. */
  535. if (comments.size() == 0) {
  536. add(new Label("discussion").setVisible(false));
  537. } else {
  538. Fragment discussionFragment = new Fragment("discussion", "discussionFragment", this);
  539. ListDataProvider<Change> discussionDp = new ListDataProvider<Change>(discussion);
  540. DataView<Change> discussionView = new DataView<Change>("discussion", discussionDp) {
  541. private static final long serialVersionUID = 1L;
  542. @Override
  543. public void populateItem(final Item<Change> item) {
  544. final Change entry = item.getModelObject();
  545. if (entry.isMerge()) {
  546. /*
  547. * MERGE
  548. */
  549. String resolvedBy = entry.getString(Field.mergeSha);
  550. // identify the merged patch, it is likely the last
  551. Patchset mergedPatch = null;
  552. for (Change c : revisions) {
  553. if (c.patchset.tip.equals(resolvedBy)) {
  554. mergedPatch = c.patchset;
  555. break;
  556. }
  557. }
  558. String commitLink;
  559. if (mergedPatch == null) {
  560. // shouldn't happen, but just-in-case
  561. int len = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);
  562. commitLink = resolvedBy.substring(0, len);
  563. } else {
  564. // expected result
  565. commitLink = mergedPatch.toString();
  566. }
  567. Fragment mergeFragment = new Fragment("entry", "mergeFragment", this);
  568. mergeFragment.add(new LinkPanel("commitLink", null, commitLink,
  569. CommitPage.class, WicketUtils.newObjectParameter(repositoryName, resolvedBy)));
  570. mergeFragment.add(new Label("toBranch", MessageFormat.format(getString("gb.toBranch"),
  571. "<b>" + ticket.mergeTo + "</b>")).setEscapeModelStrings(false));
  572. addUserAttributions(mergeFragment, entry, 0);
  573. addDateAttributions(mergeFragment, entry);
  574. item.add(mergeFragment);
  575. } else if (entry.isStatusChange()) {
  576. /*
  577. * STATUS CHANGE
  578. */
  579. Fragment frag = new Fragment("entry", "statusFragment", this);
  580. Label status = new Label("statusChange", entry.getStatus().toString());
  581. String css = getLozengeClass(entry.getStatus(), false);
  582. WicketUtils.setCssClass(status, css);
  583. for (IBehavior b : status.getBehaviors()) {
  584. if (b instanceof SimpleAttributeModifier) {
  585. SimpleAttributeModifier sam = (SimpleAttributeModifier) b;
  586. if ("class".equals(sam.getAttribute())) {
  587. status.add(new SimpleAttributeModifier("class", "status-change " + sam.getValue()));
  588. break;
  589. }
  590. }
  591. }
  592. frag.add(status);
  593. addUserAttributions(frag, entry, avatarWidth);
  594. addDateAttributions(frag, entry);
  595. item.add(frag);
  596. } else {
  597. /*
  598. * COMMENT
  599. */
  600. String comment = MarkdownUtils.transformGFM(app().settings(), entry.comment.text, repositoryName);
  601. Fragment frag = new Fragment("entry", "commentFragment", this);
  602. Label commentIcon = new Label("commentIcon");
  603. if (entry.comment.src == CommentSource.Email) {
  604. WicketUtils.setCssClass(commentIcon, "iconic-mail");
  605. } else {
  606. WicketUtils.setCssClass(commentIcon, "iconic-comment-alt2-stroke");
  607. }
  608. frag.add(commentIcon);
  609. frag.add(new Label("comment", comment).setEscapeModelStrings(false));
  610. addUserAttributions(frag, entry, avatarWidth);
  611. addDateAttributions(frag, entry);
  612. item.add(frag);
  613. }
  614. }
  615. };
  616. discussionFragment.add(discussionView);
  617. add(discussionFragment);
  618. }
  619. /*
  620. * ADD COMMENT PANEL
  621. */
  622. if (UserModel.ANONYMOUS.equals(user)
  623. || !repository.isBare
  624. || repository.isFrozen
  625. || repository.isMirror) {
  626. // prohibit comments for anonymous users, local working copy repos,
  627. // frozen repos, and mirrors
  628. add(new Label("newComment").setVisible(false));
  629. } else {
  630. // permit user to comment
  631. Fragment newComment = new Fragment("newComment", "newCommentFragment", this);
  632. GravatarImage img = new GravatarImage("newCommentAvatar", user.username, user.emailAddress,
  633. "gravatar-round", avatarWidth, true);
  634. newComment.add(img);
  635. CommentPanel commentPanel = new CommentPanel("commentPanel", user, ticket, null, TicketsPage.class);
  636. commentPanel.setRepository(repositoryName);
  637. newComment.add(commentPanel);
  638. add(newComment);
  639. }
  640. /*
  641. * PATCHSET TAB
  642. */
  643. if (currentPatchset == null) {
  644. // no patchset yet, show propose fragment
  645. String repoUrl = getRepositoryUrl(user, repository);
  646. Fragment changeIdFrag = new Fragment("patchset", "proposeFragment", this);
  647. changeIdFrag.add(new Label("proposeInstructions", MarkdownUtils.transformMarkdown(getString("gb.proposeInstructions"))).setEscapeModelStrings(false));
  648. changeIdFrag.add(new Label("ptWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Barnum")));
  649. changeIdFrag.add(new Label("ptWorkflowSteps", getProposeWorkflow("propose_pt.md", repoUrl, ticket.number)).setEscapeModelStrings(false));
  650. changeIdFrag.add(new Label("gitWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Git")));
  651. changeIdFrag.add(new Label("gitWorkflowSteps", getProposeWorkflow("propose_git.md", repoUrl, ticket.number)).setEscapeModelStrings(false));
  652. add(changeIdFrag);
  653. } else {
  654. // show current patchset
  655. Fragment patchsetFrag = new Fragment("patchset", "patchsetFragment", this);
  656. patchsetFrag.add(new Label("commitsInPatchset", MessageFormat.format(getString("gb.commitsInPatchsetN"), currentPatchset.number)));
  657. // current revision
  658. MarkupContainer panel = createPatchsetPanel("panel", repository, user);
  659. patchsetFrag.add(panel);
  660. addUserAttributions(patchsetFrag, currentRevision, avatarWidth);
  661. addUserAttributions(panel, currentRevision, 0);
  662. addDateAttributions(panel, currentRevision);
  663. // commits
  664. List<RevCommit> commits = JGitUtils.getRevLog(getRepository(), currentPatchset.base, currentPatchset.tip);
  665. ListDataProvider<RevCommit> commitsDp = new ListDataProvider<RevCommit>(commits);
  666. DataView<RevCommit> commitsView = new DataView<RevCommit>("commit", commitsDp) {
  667. private static final long serialVersionUID = 1L;
  668. @Override
  669. public void populateItem(final Item<RevCommit> item) {
  670. RevCommit commit = item.getModelObject();
  671. PersonIdent author = commit.getAuthorIdent();
  672. item.add(new GravatarImage("authorAvatar", author.getName(), author.getEmailAddress(), null, 16, false));
  673. item.add(new Label("author", commit.getAuthorIdent().getName()));
  674. item.add(new LinkPanel("commitId", null, getShortObjectId(commit.getName()),
  675. CommitPage.class, WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
  676. item.add(new LinkPanel("diff", "link", getString("gb.diff"), CommitDiffPage.class,
  677. WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
  678. item.add(new Label("title", StringUtils.trimString(commit.getShortMessage(), Constants.LEN_SHORTLOG_REFS)));
  679. item.add(WicketUtils.createDateLabel("commitDate", JGitUtils.getCommitDate(commit), GitBlitWebSession
  680. .get().getTimezone(), getTimeUtils(), false));
  681. item.add(new DiffStatPanel("commitDiffStat", 0, 0, true));
  682. }
  683. };
  684. patchsetFrag.add(commitsView);
  685. add(patchsetFrag);
  686. }
  687. /*
  688. * ACTIVITY TAB
  689. */
  690. Fragment revisionHistory = new Fragment("activity", "activityFragment", this);
  691. List<Change> events = new ArrayList<Change>(ticket.changes);
  692. Collections.sort(events);
  693. Collections.reverse(events);
  694. ListDataProvider<Change> eventsDp = new ListDataProvider<Change>(events);
  695. DataView<Change> eventsView = new DataView<Change>("event", eventsDp) {
  696. private static final long serialVersionUID = 1L;
  697. @Override
  698. public void populateItem(final Item<Change> item) {
  699. Change event = item.getModelObject();
  700. addUserAttributions(item, event, 16);
  701. if (event.hasPatchset()) {
  702. // patchset
  703. Patchset patchset = event.patchset;
  704. String what;
  705. if (event.isStatusChange() && (Status.New == event.getStatus())) {
  706. what = getString("gb.proposedThisChange");
  707. } else if (patchset.rev == 1) {
  708. what = MessageFormat.format(getString("gb.uploadedPatchsetN"), patchset.number);
  709. } else {
  710. if (patchset.added == 1) {
  711. what = getString("gb.addedOneCommit");
  712. } else {
  713. what = MessageFormat.format(getString("gb.addedNCommits"), patchset.added);
  714. }
  715. }
  716. item.add(new Label("what", what));
  717. LinkPanel psr = new LinkPanel("patchsetRevision", null, patchset.number + "-" + patchset.rev,
  718. ComparePage.class, WicketUtils.newRangeParameter(repositoryName, patchset.parent == null ? patchset.base : patchset.parent, patchset.tip), true);
  719. WicketUtils.setHtmlTooltip(psr, patchset.toString());
  720. item.add(psr);
  721. String typeCss = getPatchsetTypeCss(patchset.type);
  722. Label typeLabel = new Label("patchsetType", patchset.type.toString());
  723. if (typeCss == null) {
  724. typeLabel.setVisible(false);
  725. } else {
  726. WicketUtils.setCssClass(typeLabel, typeCss);
  727. }
  728. item.add(typeLabel);
  729. // show commit diffstat
  730. item.add(new DiffStatPanel("patchsetDiffStat", patchset.insertions, patchset.deletions, patchset.rev > 1));
  731. } else if (event.hasComment()) {
  732. // comment
  733. item.add(new Label("what", getString("gb.commented")));
  734. item.add(new Label("patchsetRevision").setVisible(false));
  735. item.add(new Label("patchsetType").setVisible(false));
  736. item.add(new Label("patchsetDiffStat").setVisible(false));
  737. } else if (event.hasReview()) {
  738. // review
  739. String score;
  740. switch (event.review.score) {
  741. case approved:
  742. score = "<span style='color:darkGreen'>" + getScoreDescription(event.review.score) + "</span>";
  743. break;
  744. case vetoed:
  745. score = "<span style='color:darkRed'>" + getScoreDescription(event.review.score) + "</span>";
  746. break;
  747. default:
  748. score = getScoreDescription(event.review.score);
  749. }
  750. item.add(new Label("what", MessageFormat.format(getString("gb.reviewedPatchsetRev"),
  751. event.review.patchset, event.review.rev, score))
  752. .setEscapeModelStrings(false));
  753. item.add(new Label("patchsetRevision").setVisible(false));
  754. item.add(new Label("patchsetType").setVisible(false));
  755. item.add(new Label("patchsetDiffStat").setVisible(false));
  756. } else {
  757. // field change
  758. item.add(new Label("patchsetRevision").setVisible(false));
  759. item.add(new Label("patchsetType").setVisible(false));
  760. item.add(new Label("patchsetDiffStat").setVisible(false));
  761. String what = "";
  762. if (event.isStatusChange()) {
  763. switch (event.getStatus()) {
  764. case New:
  765. if (ticket.isProposal()) {
  766. what = getString("gb.proposedThisChange");
  767. } else {
  768. what = getString("gb.createdThisTicket");
  769. }
  770. break;
  771. default:
  772. break;
  773. }
  774. }
  775. item.add(new Label("what", what).setVisible(what.length() > 0));
  776. }
  777. addDateAttributions(item, event);
  778. if (event.hasFieldChanges()) {
  779. StringBuilder sb = new StringBuilder();
  780. sb.append("<table class=\"summary\"><tbody>");
  781. for (Map.Entry<Field, String> entry : event.fields.entrySet()) {
  782. String value;
  783. switch (entry.getKey()) {
  784. case body:
  785. String body = entry.getValue();
  786. if (event.isStatusChange() && Status.New == event.getStatus() && StringUtils.isEmpty(body)) {
  787. // ignore initial empty description
  788. continue;
  789. }
  790. // trim body changes
  791. if (StringUtils.isEmpty(body)) {
  792. value = "<i>" + ESC_NIL + "</i>";
  793. } else {
  794. value = StringUtils.trimString(body, Constants.LEN_SHORTLOG_REFS);
  795. }
  796. break;
  797. case status:
  798. // special handling for status
  799. Status status = event.getStatus();
  800. String css = getLozengeClass(status, true);
  801. value = String.format("<span class=\"%1$s\">%2$s</span>", css, status.toString());
  802. break;
  803. default:
  804. value = StringUtils.isEmpty(entry.getValue()) ? ("<i>" + ESC_NIL + "</i>") : StringUtils.escapeForHtml(entry.getValue(), false);
  805. break;
  806. }
  807. sb.append("<tr><th style=\"width:70px;\">");
  808. sb.append(entry.getKey().name());
  809. sb.append("</th><td>");
  810. sb.append(value);
  811. sb.append("</td></tr>");
  812. }
  813. sb.append("</tbody></table>");
  814. item.add(new Label("fields", sb.toString()).setEscapeModelStrings(false));
  815. } else {
  816. item.add(new Label("fields").setVisible(false));
  817. }
  818. }
  819. };
  820. revisionHistory.add(eventsView);
  821. add(revisionHistory);
  822. }
  823. protected void addUserAttributions(MarkupContainer container, Change entry, int avatarSize) {
  824. UserModel commenter = app().users().getUserModel(entry.author);
  825. if (commenter == null) {
  826. // unknown user
  827. container.add(new GravatarImage("changeAvatar", entry.author,
  828. entry.author, null, avatarSize, false).setVisible(avatarSize > 0));
  829. container.add(new Label("changeAuthor", entry.author.toLowerCase()));
  830. } else {
  831. // known user
  832. container.add(new GravatarImage("changeAvatar", commenter.getDisplayName(),
  833. commenter.emailAddress, avatarSize > 24 ? "gravatar-round" : null,
  834. avatarSize, true).setVisible(avatarSize > 0));
  835. container.add(new LinkPanel("changeAuthor", null, commenter.getDisplayName(),
  836. UserPage.class, WicketUtils.newUsernameParameter(commenter.username)));
  837. }
  838. }
  839. protected void addDateAttributions(MarkupContainer container, Change entry) {
  840. container.add(WicketUtils.createDateLabel("changeDate", entry.date, GitBlitWebSession
  841. .get().getTimezone(), getTimeUtils(), false));
  842. // set the id attribute
  843. if (entry.hasComment()) {
  844. container.setOutputMarkupId(true);
  845. container.add(new AttributeModifier("id", Model.of(entry.getId())));
  846. ExternalLink link = new ExternalLink("changeLink", "#" + entry.getId());
  847. container.add(link);
  848. } else {
  849. container.add(new Label("changeLink").setVisible(false));
  850. }
  851. }
  852. protected String getProposeWorkflow(String resource, String url, long ticketId) {
  853. String md = readResource(resource);
  854. md = md.replace("${url}", url);
  855. md = md.replace("${repo}", StringUtils.getLastPathElement(StringUtils.stripDotGit(repositoryName)));
  856. md = md.replace("${ticketId}", "" + ticketId);
  857. md = md.replace("${patchset}", "" + 1);
  858. md = md.replace("${reviewBranch}", Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticketId)));
  859. md = md.replace("${integrationBranch}", Repository.shortenRefName(getRepositoryModel().HEAD));
  860. return MarkdownUtils.transformMarkdown(md);
  861. }
  862. protected Fragment createPatchsetPanel(String wicketId, RepositoryModel repository, UserModel user) {
  863. final Patchset currentPatchset = ticket.getCurrentPatchset();
  864. List<Patchset> patchsets = new ArrayList<Patchset>(ticket.getPatchsetRevisions(currentPatchset.number));
  865. patchsets.remove(currentPatchset);
  866. Collections.reverse(patchsets);
  867. Fragment panel = new Fragment(wicketId, "collapsiblePatchsetFragment", this);
  868. // patchset header
  869. String ps = "<b>" + currentPatchset.number + "</b>";
  870. if (currentPatchset.rev == 1) {
  871. panel.add(new Label("uploadedWhat", MessageFormat.format(getString("gb.uploadedPatchsetN"), ps)).setEscapeModelStrings(false));
  872. } else {
  873. String rev = "<b>" + currentPatchset.rev + "</b>";
  874. panel.add(new Label("uploadedWhat", MessageFormat.format(getString("gb.uploadedPatchsetNRevisionN"), ps, rev)).setEscapeModelStrings(false));
  875. }
  876. panel.add(new LinkPanel("patchId", null, "rev " + currentPatchset.rev,
  877. CommitPage.class, WicketUtils.newObjectParameter(repositoryName, currentPatchset.tip), true));
  878. // compare menu
  879. panel.add(new LinkPanel("compareMergeBase", null, getString("gb.compareToMergeBase"),
  880. ComparePage.class, WicketUtils.newRangeParameter(repositoryName, currentPatchset.base, currentPatchset.tip), true));
  881. ListDataProvider<Patchset> compareMenuDp = new ListDataProvider<Patchset>(patchsets);
  882. DataView<Patchset> compareMenu = new DataView<Patchset>("comparePatch", compareMenuDp) {
  883. private static final long serialVersionUID = 1L;
  884. @Override
  885. public void populateItem(final Item<Patchset> item) {
  886. Patchset patchset = item.getModelObject();
  887. LinkPanel link = new LinkPanel("compareLink", null,
  888. MessageFormat.format(getString("gb.compareToN"), patchset.number + "-" + patchset.rev),
  889. ComparePage.class, WicketUtils.newRangeParameter(getRepositoryModel().name,
  890. patchset.tip, currentPatchset.tip), true);
  891. item.add(link);
  892. }
  893. };
  894. panel.add(compareMenu);
  895. // reviews
  896. List<Change> reviews = ticket.getReviews(currentPatchset);
  897. ListDataProvider<Change> reviewsDp = new ListDataProvider<Change>(reviews);
  898. DataView<Change> reviewsView = new DataView<Change>("reviews", reviewsDp) {
  899. private static final long serialVersionUID = 1L;
  900. @Override
  901. public void populateItem(final Item<Change> item) {
  902. Change change = item.getModelObject();
  903. final String username = change.author;
  904. UserModel user = app().users().getUserModel(username);
  905. if (user == null) {
  906. item.add(new Label("reviewer", username));
  907. } else {
  908. item.add(new LinkPanel("reviewer", null, user.getDisplayName(),
  909. UserPage.class, WicketUtils.newUsernameParameter(username)));
  910. }
  911. // indicate review score
  912. Review review = change.review;
  913. Label scoreLabel = new Label("score");
  914. String scoreClass = getScoreClass(review.score);
  915. String tooltip = getScoreDescription(review.score);
  916. WicketUtils.setCssClass(scoreLabel, scoreClass);
  917. if (!StringUtils.isEmpty(tooltip)) {
  918. WicketUtils.setHtmlTooltip(scoreLabel, tooltip);
  919. }
  920. item.add(scoreLabel);
  921. }
  922. };
  923. panel.add(reviewsView);
  924. if (ticket.isOpen() && user.canReviewPatchset(repository)) {
  925. // can only review open tickets
  926. Review myReview = null;
  927. for (Change change : ticket.getReviews(currentPatchset)) {
  928. if (change.author.equals(user.username)) {
  929. myReview = change.review;
  930. }
  931. }
  932. // user can review, add review controls
  933. Fragment reviewControls = new Fragment("reviewControls", "reviewControlsFragment", this);
  934. // show "approve" button if no review OR not current score
  935. if (user.canApprovePatchset(repository) && (myReview == null || Score.approved != myReview.score)) {
  936. reviewControls.add(createReviewLink("approveLink", Score.approved));
  937. } else {
  938. reviewControls.add(new Label("approveLink").setVisible(false));
  939. }
  940. // show "looks good" button if no review OR not current score
  941. if (myReview == null || Score.looks_good != myReview.score) {
  942. reviewControls.add(createReviewLink("looksGoodLink", Score.looks_good));
  943. } else {
  944. reviewControls.add(new Label("looksGoodLink").setVisible(false));
  945. }
  946. // show "needs improvement" button if no review OR not current score
  947. if (myReview == null || Score.needs_improvement != myReview.score) {
  948. reviewControls.add(createReviewLink("needsImprovementLink", Score.needs_improvement));
  949. } else {
  950. reviewControls.add(new Label("needsImprovementLink").setVisible(false));
  951. }
  952. // show "veto" button if no review OR not current score
  953. if (user.canVetoPatchset(repository) && (myReview == null || Score.vetoed != myReview.score)) {
  954. reviewControls.add(createReviewLink("vetoLink", Score.vetoed));
  955. } else {
  956. reviewControls.add(new Label("vetoLink").setVisible(false));
  957. }
  958. panel.add(reviewControls);
  959. } else {
  960. // user can not review
  961. panel.add(new Label("reviewControls").setVisible(false));
  962. }
  963. String insertions = MessageFormat.format("<span style=\"color:darkGreen;font-weight:bold;\">+{0}</span>", ticket.insertions);
  964. String deletions = MessageFormat.format("<span style=\"color:darkRed;font-weight:bold;\">-{0}</span>", ticket.deletions);
  965. panel.add(new Label("patchsetStat", MessageFormat.format(StringUtils.escapeForHtml(getString("gb.diffStat"), false),
  966. insertions, deletions)).setEscapeModelStrings(false));
  967. // changed paths list
  968. List<PathChangeModel> paths = JGitUtils.getFilesInRange(getRepository(), currentPatchset.base, currentPatchset.tip);
  969. ListDataProvider<PathChangeModel> pathsDp = new ListDataProvider<PathChangeModel>(paths);
  970. DataView<PathChangeModel> pathsView = new DataView<PathChangeModel>("changedPath", pathsDp) {
  971. private static final long serialVersionUID = 1L;
  972. int counter;
  973. @Override
  974. public void populateItem(final Item<PathChangeModel> item) {
  975. final PathChangeModel entry = item.getModelObject();
  976. Label changeType = new Label("changeType", "");
  977. WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);
  978. setChangeTypeTooltip(changeType, entry.changeType);
  979. item.add(changeType);
  980. item.add(new DiffStatPanel("diffStat", entry.insertions, entry.deletions, true));
  981. boolean hasSubmodule = false;
  982. String submodulePath = null;
  983. if (entry.isTree()) {
  984. // tree
  985. item.add(new LinkPanel("pathName", null, entry.path, TreePage.class,
  986. WicketUtils
  987. .newPathParameter(repositoryName, currentPatchset.tip, entry.path), true));
  988. item.add(new Label("diffStat").setVisible(false));
  989. } else if (entry.isSubmodule()) {
  990. // submodule
  991. String submoduleId = entry.objectId;
  992. SubmoduleModel submodule = getSubmodule(entry.path);
  993. submodulePath = submodule.gitblitPath;
  994. hasSubmodule = submodule.hasSubmodule;
  995. item.add(new LinkPanel("pathName", "list", entry.path + " @ " +
  996. getShortObjectId(submoduleId), TreePage.class,
  997. WicketUtils.newPathParameter(submodulePath, submoduleId, ""), true).setEnabled(hasSubmodule));
  998. item.add(new Label("diffStat").setVisible(false));
  999. } else {
  1000. // blob
  1001. String displayPath = entry.path;
  1002. String path = entry.path;
  1003. if (entry.isSymlink()) {
  1004. RevCommit commit = JGitUtils.getCommit(getRepository(), Constants.R_TICKETS_PATCHSETS + ticket.number);
  1005. path = JGitUtils.getStringContent(getRepository(), commit.getTree(), path);
  1006. displayPath = entry.path + " -> " + path;
  1007. }
  1008. if (entry.changeType.equals(ChangeType.ADD)) {
  1009. // add show view
  1010. item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
  1011. WicketUtils.newPathParameter(repositoryName, currentPatchset.tip, path), true));
  1012. } else if (entry.changeType.equals(ChangeType.DELETE)) {
  1013. // delete, show label
  1014. item.add(new Label("pathName", displayPath));
  1015. } else {
  1016. // mod, show diff
  1017. item.add(new LinkPanel("pathName", "list", displayPath, BlobDiffPage.class,
  1018. WicketUtils.newPathParameter(repositoryName, currentPatchset.tip, path), true));
  1019. }
  1020. }
  1021. // quick links
  1022. if (entry.isSubmodule()) {
  1023. // submodule
  1024. item.add(setNewTarget(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
  1025. .newPathParameter(repositoryName, entry.commitId, entry.path)))
  1026. .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
  1027. item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
  1028. .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
  1029. } else {
  1030. // tree or blob
  1031. item.add(setNewTarget(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
  1032. .newBlobDiffParameter(repositoryName, currentPatchset.base, currentPatchset.tip, entry.path)))
  1033. .setEnabled(!entry.changeType.equals(ChangeType.ADD)
  1034. && !entry.changeType.equals(ChangeType.DELETE)));
  1035. item.add(setNewTarget(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
  1036. .newPathParameter(repositoryName, currentPatchset.tip, entry.path)))
  1037. .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
  1038. }
  1039. WicketUtils.setAlternatingBackground(item, counter);
  1040. counter++;
  1041. }
  1042. };
  1043. panel.add(pathsView);
  1044. addPtReviewInstructions(user, repository, panel);
  1045. addGitReviewInstructions(user, repository, panel);
  1046. panel.add(createMergePanel(user, repository));
  1047. return panel;
  1048. }
  1049. protected IconAjaxLink<String> createReviewLink(String wicketId, final Score score) {
  1050. return new IconAjaxLink<String>(wicketId, getScoreClass(score), Model.of(getScoreDescription(score))) {
  1051. private static final long serialVersionUID = 1L;
  1052. @Override
  1053. public void onClick(AjaxRequestTarget target) {
  1054. review(score);
  1055. }
  1056. };
  1057. }
  1058. protected String getScoreClass(Score score) {
  1059. switch (score) {
  1060. case vetoed:
  1061. return "fa fa-exclamation-circle";
  1062. case needs_improvement:
  1063. return "fa fa-thumbs-o-down";
  1064. case looks_good:
  1065. return "fa fa-thumbs-o-up";
  1066. case approved:
  1067. return "fa fa-check-circle";
  1068. case not_reviewed:
  1069. default:
  1070. return "fa fa-minus-circle";
  1071. }
  1072. }
  1073. protected String getScoreDescription(Score score) {
  1074. String description;
  1075. switch (score) {
  1076. case vetoed:
  1077. description = getString("gb.veto");
  1078. break;
  1079. case needs_improvement:
  1080. description = getString("gb.needsImprovement");
  1081. break;
  1082. case looks_good:
  1083. description = getString("gb.looksGood");
  1084. break;
  1085. case approved:
  1086. description = getString("gb.approve");
  1087. break;
  1088. case not_reviewed:
  1089. default:
  1090. description = getString("gb.hasNotReviewed");
  1091. }
  1092. return String.format("%1$s (%2$+d)", description, score.getValue());
  1093. }
  1094. protected void review(Score score) {
  1095. UserModel user = GitBlitWebSession.get().getUser();
  1096. Patchset ps = ticket.getCurrentPatchset();
  1097. Change change = new Change(user.username);
  1098. change.review(ps, score, !ticket.isReviewer(user.username));
  1099. if (!ticket.isWatching(user.username)) {
  1100. change.watch(user.username);
  1101. }
  1102. TicketModel updatedTicket = app().tickets().updateTicket(getRepositoryModel(), ticket.number, change);
  1103. app().tickets().createNotifier().sendMailing(updatedTicket);
  1104. setResponsePage(TicketsPage.class, getPageParameters());
  1105. }
  1106. protected <X extends MarkupContainer> X setNewTarget(X x) {
  1107. x.add(new SimpleAttributeModifier("target", "_blank"));
  1108. return x;
  1109. }
  1110. protected void addGitReviewInstructions(UserModel user, RepositoryModel repository, MarkupContainer panel) {
  1111. String repoUrl = getRepositoryUrl(user, repository);
  1112. panel.add(new Label("gitStep1", MessageFormat.format(getString("gb.stepN"), 1)));
  1113. panel.add(new Label("gitStep2", MessageFormat.format(getString("gb.stepN"), 2)));
  1114. String ticketBranch = Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticket.number));
  1115. String reviewBranch = PatchsetCommand.getReviewBranch(ticket.number);
  1116. String step1 = MessageFormat.format("git fetch {0} {1}", repoUrl, ticketBranch);
  1117. String step2 = MessageFormat.format("git checkout -B {0} FETCH_HEAD", reviewBranch);
  1118. panel.add(new Label("gitPreStep1", step1));
  1119. panel.add(new Label("gitPreStep2", step2));
  1120. panel.add(createCopyFragment("gitCopyStep1", step1.replace("\n", " && ")));
  1121. panel.add(createCopyFragment("gitCopyStep2", step2.replace("\n", " && ")));
  1122. }
  1123. protected void addPtReviewInstructions(UserModel user, RepositoryModel repository, MarkupContainer panel) {
  1124. String step1 = MessageFormat.format("pt checkout {0,number,0}", ticket.number);
  1125. panel.add(new Label("ptPreStep", step1));
  1126. panel.add(createCopyFragment("ptCopyStep", step1));
  1127. }
  1128. /**
  1129. * Adds a merge panel for the patchset to the markup container. The panel
  1130. * may just a message if the patchset can not be merged.
  1131. *
  1132. * @param c
  1133. * @param user
  1134. * @param repository
  1135. */
  1136. protected Component createMergePanel(UserModel user, RepositoryModel repository) {
  1137. Patchset patchset = ticket.getCurrentPatchset();
  1138. if (patchset == null) {
  1139. // no patchset to merge
  1140. return new Label("mergePanel");
  1141. }
  1142. boolean allowMerge;
  1143. if (repository.requireApproval) {
  1144. // rpeository requires approval
  1145. allowMerge = ticket.isOpen() && ticket.isApproved(patchset);
  1146. } else {
  1147. // vetos are binding
  1148. allowMerge = ticket.isOpen() && !ticket.isVetoed(patchset);
  1149. }
  1150. MergeStatus mergeStatus = JGitUtils.canMerge(getRepository(), patchset.tip, ticket.mergeTo);
  1151. if (allowMerge) {
  1152. if (MergeStatus.MERGEABLE == mergeStatus) {
  1153. // patchset can be cleanly merged to integration branch OR has already been merged
  1154. Fragment mergePanel = new Fragment("mergePanel", "mergeableFragment", this);
  1155. mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetMergeable"), ticket.mergeTo)));
  1156. if (user.canPush(repository)) {
  1157. // user can merge locally
  1158. SimpleAjaxLink<String> mergeButton = new SimpleAjaxLink<String>("mergeButton", Model.of(getString("gb.merge"))) {
  1159. private static final long serialVersionUID = 1L;
  1160. @Override
  1161. public void onClick(AjaxRequestTarget target) {
  1162. // ensure the patchset is still current AND not vetoed
  1163. Patchset patchset = ticket.getCurrentPatchset();
  1164. final TicketModel refreshedTicket = app().tickets().getTicket(getRepositoryModel(), ticket.number);
  1165. if (patchset.equals(refreshedTicket.getCurrentPatchset())) {
  1166. // patchset is current, check for recent veto
  1167. if (!refreshedTicket.isVetoed(patchset)) {
  1168. // patchset is not vetoed
  1169. // execute the merge using the ticket service
  1170. app().tickets().exec(new Runnable() {
  1171. @Override
  1172. public void run() {
  1173. PatchsetReceivePack rp = new PatchsetReceivePack(
  1174. app().gitblit(),
  1175. getRepository(),
  1176. getRepositoryModel(),
  1177. GitBlitWebSession.get().getUser());
  1178. MergeStatus result = rp.merge(refreshedTicket);
  1179. if (MergeStatus.MERGED == result) {
  1180. // notify participants and watchers
  1181. rp.sendAll();
  1182. } else {
  1183. // merge failure
  1184. String msg = MessageFormat.format("Failed to merge ticket {0,number,0}: {1}", ticket.number, result.name());
  1185. logger.error(msg);
  1186. GitBlitWebSession.get().cacheErrorMessage(msg);
  1187. }
  1188. }
  1189. });
  1190. } else {
  1191. // vetoed patchset
  1192. String msg = MessageFormat.format("Can not merge ticket {0,number,0}, patchset {1,number,0} has been vetoed!",
  1193. ticket.number, patchset.number);
  1194. GitBlitWebSession.get().cacheErrorMessage(msg);
  1195. logger.error(msg);
  1196. }
  1197. } else {
  1198. // not current patchset
  1199. String msg = MessageFormat.format("Can not merge ticket {0,number,0}, the patchset has been updated!", ticket.number);
  1200. GitBlitWebSession.get().cacheErrorMessage(msg);
  1201. logger.error(msg);
  1202. }
  1203. setResponsePage(TicketsPage.class, getPageParameters());
  1204. }
  1205. };
  1206. mergePanel.add(mergeButton);
  1207. Component instructions = getMergeInstructions(user, repository, "mergeMore", "gb.patchsetMergeableMore");
  1208. mergePanel.add(instructions);
  1209. } else {
  1210. mergePanel.add(new Label("mergeButton").setVisible(false));
  1211. mergePanel.add(new Label("mergeMore").setVisible(false));
  1212. }
  1213. return mergePanel;
  1214. } else if (MergeStatus.ALREADY_MERGED == mergeStatus) {
  1215. // patchset already merged
  1216. Fragment mergePanel = new Fragment("mergePanel", "alreadyMergedFragment", this);
  1217. mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetAlreadyMerged"), ticket.mergeTo)));
  1218. return mergePanel;
  1219. } else {
  1220. // patchset can not be cleanly merged
  1221. Fragment mergePanel = new Fragment("mergePanel", "notMergeableFragment", this);
  1222. mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
  1223. if (user.canPush(repository)) {
  1224. // user can merge locally
  1225. Component instructions = getMergeInstructions(user, repository, "mergeMore", "gb.patchsetNotMergeableMore");
  1226. mergePanel.add(instructions);
  1227. } else {
  1228. mergePanel.add(new Label("mergeMore").setVisible(false));
  1229. }
  1230. return mergePanel;
  1231. }
  1232. } else {
  1233. // merge not allowed
  1234. if (MergeStatus.ALREADY_MERGED == mergeStatus) {
  1235. // patchset already merged
  1236. Fragment mergePanel = new Fragment("mergePanel", "alreadyMergedFragment", this);
  1237. mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetAlreadyMerged"), ticket.mergeTo)));
  1238. return mergePanel;
  1239. } else if (ticket.isVetoed(patchset)) {
  1240. // patchset has been vetoed
  1241. Fragment mergePanel = new Fragment("mergePanel", "vetoedFragment", this);
  1242. mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
  1243. return mergePanel;
  1244. } else if (repository.requireApproval) {
  1245. // patchset has been not been approved for merge
  1246. Fragment mergePanel = new Fragment("mergePanel", "notApprovedFragment", this);
  1247. mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotApproved"), ticket.mergeTo)));
  1248. mergePanel.add(new Label("mergeMore", MessageFormat.format(getString("gb.patchsetNotApprovedMore"), ticket.mergeTo)));
  1249. return mergePanel;
  1250. } else {
  1251. // other case
  1252. return new Label("mergePanel");
  1253. }
  1254. }
  1255. }
  1256. protected Component getMergeInstructions(UserModel user, RepositoryModel repository, String markupId, String infoKey) {
  1257. Fragment cmd = new Fragment(markupId, "commandlineMergeFragment", this);
  1258. cmd.add(new Label("instructions", MessageFormat.format(getString(infoKey), ticket.mergeTo)));
  1259. String repoUrl = getRepositoryUrl(user, repository);
  1260. // git instructions
  1261. cmd.add(new Label("mergeStep1", MessageFormat.format(getString("gb.stepN"), 1)));
  1262. cmd.add(new Label("mergeStep2", MessageFormat.format(getString("gb.stepN"), 2)));
  1263. cmd.add(new Label("mergeStep3", MessageFormat.format(getString("gb.stepN"), 3)));
  1264. String ticketBranch = Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticket.number));
  1265. String reviewBranch = PatchsetCommand.getReviewBranch(ticket.number);
  1266. String step1 = MessageFormat.format("git checkout -B {0} {1}", reviewBranch, ticket.mergeTo);
  1267. String step2 = MessageFormat.format("git pull {0} {1}", repoUrl, ticketBranch);
  1268. String step3 = MessageFormat.format("git checkout {0}\ngit merge {1}\ngit push origin {0}", ticket.mergeTo, reviewBranch);
  1269. cmd.add(new Label("mergePreStep1", step1));
  1270. cmd.add(new Label("mergePreStep2", step2));
  1271. cmd.add(new Label("mergePreStep3", step3));
  1272. cmd.add(createCopyFragment("mergeCopyStep1", step1.replace("\n", " && ")));
  1273. cmd.add(createCopyFragment("mergeCopyStep2", step2.replace("\n", " && ")));
  1274. cmd.add(createCopyFragment("mergeCopyStep3", step3.replace("\n", " && ")));
  1275. // pt instructions
  1276. String ptStep = MessageFormat.format("pt pull {0,number,0}", ticket.number);
  1277. cmd.add(new Label("ptMergeStep", ptStep));
  1278. cmd.add(createCopyFragment("ptMergeCopyStep", step1.replace("\n", " && ")));
  1279. return cmd;
  1280. }
  1281. /**
  1282. * Returns the primary repository url
  1283. *
  1284. * @param user
  1285. * @param repository
  1286. * @return the primary repository url
  1287. */
  1288. protected String getRepositoryUrl(UserModel user, RepositoryModel repository) {
  1289. HttpServletRequest req = ((WebRequest) getRequest()).getHttpServletRequest();
  1290. String primaryurl = app().gitblit().getRepositoryUrls(req, user, repository).get(0).url;
  1291. String url = primaryurl;
  1292. try {
  1293. url = new URIish(primaryurl).setUser(null).toString();
  1294. } catch (Exception e) {
  1295. }
  1296. return url;
  1297. }
  1298. /**
  1299. * Returns the ticket (if any) that this commit references.
  1300. *
  1301. * @param commit
  1302. * @return null or a ticket
  1303. */
  1304. protected TicketModel getTicket(RevCommit commit) {
  1305. try {
  1306. Map<String, Ref> refs = getRepository().getRefDatabase().getRefs(Constants.R_TICKETS_PATCHSETS);
  1307. for (Map.Entry<String, Ref> entry : refs.entrySet()) {
  1308. if (entry.getValue().getObjectId().equals(commit.getId())) {
  1309. long id = PatchsetCommand.getTicketNumber(entry.getKey());
  1310. TicketModel ticket = app().tickets().getTicket(getRepositoryModel(), id);
  1311. return ticket;
  1312. }
  1313. }
  1314. } catch (Exception e) {
  1315. logger().error("failed to determine ticket from ref", e);
  1316. }
  1317. return null;
  1318. }
  1319. protected String getPatchsetTypeCss(PatchsetType type) {
  1320. String typeCss;
  1321. switch (type) {
  1322. case Rebase:
  1323. case Rebase_Squash:
  1324. typeCss = getLozengeClass(Status.Declined, false);
  1325. break;
  1326. case Squash:
  1327. case Amend:
  1328. typeCss = getLozengeClass(Status.On_Hold, false);
  1329. break;
  1330. case Proposal:
  1331. typeCss = getLozengeClass(Status.New, false);
  1332. break;
  1333. case FastForward:
  1334. default:
  1335. typeCss = null;
  1336. break;
  1337. }
  1338. return typeCss;
  1339. }
  1340. @Override
  1341. protected String getPageName() {
  1342. return getString("gb.ticket");
  1343. }
  1344. @Override
  1345. protected Class<? extends BasePage> getRepoNavPageClass() {
  1346. return TicketsPage.class;
  1347. }
  1348. @Override
  1349. protected String getPageTitle(String repositoryName) {
  1350. return "#" + ticket.number + " - " + ticket.title;
  1351. }
  1352. protected Fragment createCopyFragment(String wicketId, String text) {
  1353. if (app().settings().getBoolean(Keys.web.allowFlashCopyToClipboard, true)) {
  1354. // clippy: flash-based copy & paste
  1355. Fragment copyFragment = new Fragment(wicketId, "clippyPanel", this);
  1356. String baseUrl = WicketUtils.getGitblitURL(getRequest());
  1357. ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf");
  1358. clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(text));
  1359. copyFragment.add(clippy);
  1360. return copyFragment;
  1361. } else {
  1362. // javascript: manual copy & paste with modal browser prompt dialog
  1363. Fragment copyFragment = new Fragment(wicketId, "jsPanel", this);
  1364. ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png");
  1365. img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", text));
  1366. copyFragment.add(img);
  1367. return copyFragment;
  1368. }
  1369. }
  1370. }