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.

TicketPage.java 61KB

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