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 |
- /*
- * Copyright 2014 gitblit.com.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- package com.gitblit.wicket.pages;
-
- import java.text.DateFormat;
- import java.text.MessageFormat;
- import java.text.SimpleDateFormat;
- import java.util.ArrayList;
- import java.util.Arrays;
- import java.util.Calendar;
- import java.util.Collections;
- import java.util.Date;
- import java.util.List;
- import java.util.Map;
- import java.util.Set;
- import java.util.TimeZone;
- import java.util.TreeSet;
-
- import javax.servlet.http.HttpServletRequest;
-
- import org.apache.wicket.AttributeModifier;
- import org.apache.wicket.Component;
- import org.apache.wicket.MarkupContainer;
- import org.apache.wicket.PageParameters;
- import org.apache.wicket.RestartResponseException;
- import org.apache.wicket.ajax.AjaxRequestTarget;
- import org.apache.wicket.behavior.IBehavior;
- import org.apache.wicket.behavior.SimpleAttributeModifier;
- import org.apache.wicket.markup.html.basic.Label;
- import org.apache.wicket.markup.html.image.ContextImage;
- import org.apache.wicket.markup.html.link.BookmarkablePageLink;
- import org.apache.wicket.markup.html.link.ExternalLink;
- import org.apache.wicket.markup.html.panel.Fragment;
- import org.apache.wicket.markup.repeater.Item;
- import org.apache.wicket.markup.repeater.data.DataView;
- import org.apache.wicket.markup.repeater.data.ListDataProvider;
- import org.apache.wicket.model.Model;
- import org.apache.wicket.protocol.http.WebRequest;
- import org.eclipse.jgit.diff.DiffEntry.ChangeType;
- import org.eclipse.jgit.lib.PersonIdent;
- import org.eclipse.jgit.lib.Ref;
- import org.eclipse.jgit.lib.Repository;
- import org.eclipse.jgit.revwalk.RevCommit;
- import org.eclipse.jgit.transport.URIish;
-
- import com.gitblit.Constants;
- import com.gitblit.Constants.AccessPermission;
- import com.gitblit.Keys;
- import com.gitblit.git.PatchsetCommand;
- import com.gitblit.git.PatchsetReceivePack;
- import com.gitblit.models.PathModel.PathChangeModel;
- import com.gitblit.models.RegistrantAccessPermission;
- import com.gitblit.models.RepositoryModel;
- import com.gitblit.models.SubmoduleModel;
- import com.gitblit.models.TicketModel;
- import com.gitblit.models.TicketModel.Change;
- import com.gitblit.models.TicketModel.CommentSource;
- import com.gitblit.models.TicketModel.Field;
- import com.gitblit.models.TicketModel.Patchset;
- import com.gitblit.models.TicketModel.PatchsetType;
- import com.gitblit.models.TicketModel.Review;
- import com.gitblit.models.TicketModel.Score;
- import com.gitblit.models.TicketModel.Status;
- import com.gitblit.models.UserModel;
- import com.gitblit.tickets.TicketIndexer.Lucene;
- import com.gitblit.tickets.TicketLabel;
- import com.gitblit.tickets.TicketMilestone;
- import com.gitblit.tickets.TicketResponsible;
- import com.gitblit.utils.JGitUtils;
- import com.gitblit.utils.JGitUtils.MergeStatus;
- import com.gitblit.utils.MarkdownUtils;
- import com.gitblit.utils.StringUtils;
- import com.gitblit.utils.TimeUtils;
- import com.gitblit.wicket.GitBlitWebSession;
- import com.gitblit.wicket.WicketUtils;
- import com.gitblit.wicket.panels.BasePanel.JavascriptTextPrompt;
- import com.gitblit.wicket.panels.CommentPanel;
- import com.gitblit.wicket.panels.DiffStatPanel;
- import com.gitblit.wicket.panels.GravatarImage;
- import com.gitblit.wicket.panels.IconAjaxLink;
- import com.gitblit.wicket.panels.LinkPanel;
- import com.gitblit.wicket.panels.ShockWaveComponent;
- import com.gitblit.wicket.panels.SimpleAjaxLink;
-
- /**
- * The ticket page handles viewing and updating a ticket.
- *
- * @author James Moger
- *
- */
- public class TicketPage extends TicketBasePage {
-
- static final String NIL = "<nil>";
-
- static final String ESC_NIL = StringUtils.escapeForHtml(NIL, false);
-
- final int avatarWidth = 40;
-
- final TicketModel ticket;
-
- public TicketPage(PageParameters params) {
- super(params);
-
- final UserModel user = GitBlitWebSession.get().getUser() == null ? UserModel.ANONYMOUS : GitBlitWebSession.get().getUser();
- final boolean isAuthenticated = !UserModel.ANONYMOUS.equals(user) && user.isAuthenticated;
- final RepositoryModel repository = getRepositoryModel();
- final String id = WicketUtils.getObject(params);
- long ticketId = Long.parseLong(id);
- ticket = app().tickets().getTicket(repository, ticketId);
-
- if (ticket == null) {
- // ticket not found
- throw new RestartResponseException(TicketsPage.class, WicketUtils.newRepositoryParameter(repositoryName));
- }
-
- final List<Change> revisions = new ArrayList<Change>();
- List<Change> comments = new ArrayList<Change>();
- List<Change> statusChanges = new ArrayList<Change>();
- List<Change> discussion = new ArrayList<Change>();
- for (Change change : ticket.changes) {
- if (change.hasComment() || (change.isStatusChange() && (change.getStatus() != Status.New))) {
- discussion.add(change);
- }
- if (change.hasComment()) {
- comments.add(change);
- }
- if (change.hasPatchset()) {
- revisions.add(change);
- }
- if (change.isStatusChange() && !change.hasPatchset()) {
- statusChanges.add(change);
- }
- }
-
- final Change currentRevision = revisions.isEmpty() ? null : revisions.get(revisions.size() - 1);
- final Patchset currentPatchset = ticket.getCurrentPatchset();
-
- /*
- * TICKET HEADER
- */
- String href = urlFor(TicketsPage.class, params).toString();
- add(new ExternalLink("ticketNumber", href, "#" + ticket.number));
- Label headerStatus = new Label("headerStatus", ticket.status.toString());
- WicketUtils.setCssClass(headerStatus, getLozengeClass(ticket.status, false));
- add(headerStatus);
- add(new Label("ticketTitle", ticket.title));
- if (currentPatchset == null) {
- add(new Label("diffstat").setVisible(false));
- } else {
- // calculate the current diffstat of the patchset
- add(new DiffStatPanel("diffstat", ticket.insertions, ticket.deletions));
- }
-
-
- /*
- * TAB TITLES
- */
- add(new Label("commentCount", "" + comments.size()).setVisible(!comments.isEmpty()));
- add(new Label("commitCount", "" + (currentPatchset == null ? 0 : currentPatchset.commits)).setVisible(currentPatchset != null));
-
-
- /*
- * TICKET AUTHOR and DATE (DISCUSSION TAB)
- */
- UserModel createdBy = app().users().getUserModel(ticket.createdBy);
- if (createdBy == null) {
- add(new Label("whoCreated", ticket.createdBy));
- } else {
- add(new LinkPanel("whoCreated", null, createdBy.getDisplayName(),
- UserPage.class, WicketUtils.newUsernameParameter(createdBy.username)));
- }
-
- if (ticket.isProposal()) {
- // clearly indicate this is a change ticket
- add(new Label("creationMessage", getString("gb.proposedThisChange")));
- } else {
- // standard ticket
- add(new Label("creationMessage", getString("gb.createdThisTicket")));
- }
-
- String dateFormat = app().settings().getString(Keys.web.datestampLongFormat, "EEEE, MMMM d, yyyy");
- String timestampFormat = app().settings().getString(Keys.web.datetimestampLongFormat, "EEEE, MMMM d, yyyy");
- final TimeZone timezone = getTimeZone();
- final DateFormat df = new SimpleDateFormat(dateFormat);
- df.setTimeZone(timezone);
- final DateFormat tsf = new SimpleDateFormat(timestampFormat);
- tsf.setTimeZone(timezone);
- final Calendar cal = Calendar.getInstance(timezone);
-
- String fuzzydate;
- TimeUtils tu = getTimeUtils();
- Date createdDate = ticket.created;
- if (TimeUtils.isToday(createdDate, timezone)) {
- fuzzydate = tu.today();
- } else if (TimeUtils.isYesterday(createdDate, timezone)) {
- fuzzydate = tu.yesterday();
- } else {
- // calculate a fuzzy time ago date
- cal.setTime(createdDate);
- cal.set(Calendar.HOUR_OF_DAY, 0);
- cal.set(Calendar.MINUTE, 0);
- cal.set(Calendar.SECOND, 0);
- cal.set(Calendar.MILLISECOND, 0);
- createdDate = cal.getTime();
- fuzzydate = getTimeUtils().timeAgo(createdDate);
- }
- Label when = new Label("whenCreated", fuzzydate + ", " + df.format(createdDate));
- WicketUtils.setHtmlTooltip(when, tsf.format(ticket.created));
- add(when);
-
- String exportHref = urlFor(ExportTicketPage.class, params).toString();
- add(new ExternalLink("exportJson", exportHref, "json"));
-
-
- /*
- * RESPONSIBLE (DISCUSSION TAB)
- */
- if (StringUtils.isEmpty(ticket.responsible)) {
- add(new Label("responsible"));
- } else {
- UserModel responsible = app().users().getUserModel(ticket.responsible);
- if (responsible == null) {
- add(new Label("responsible", ticket.responsible));
- } else {
- add(new LinkPanel("responsible", null, responsible.getDisplayName(),
- UserPage.class, WicketUtils.newUsernameParameter(responsible.username)));
- }
- }
-
- /*
- * MILESTONE PROGRESS (DISCUSSION TAB)
- */
- if (StringUtils.isEmpty(ticket.milestone)) {
- add(new Label("milestone"));
- } else {
- // link to milestone query
- TicketMilestone milestone = app().tickets().getMilestone(repository, ticket.milestone);
- PageParameters milestoneParameters = new PageParameters();
- milestoneParameters.put("r", repositoryName);
- milestoneParameters.put(Lucene.milestone.name(), ticket.milestone);
- int progress = 0;
- int open = 0;
- int closed = 0;
- if (milestone != null) {
- progress = milestone.getProgress();
- open = milestone.getOpenTickets();
- closed = milestone.getClosedTickets();
- }
-
- Fragment milestoneProgress = new Fragment("milestone", "milestoneProgressFragment", this);
- milestoneProgress.add(new LinkPanel("link", null, ticket.milestone, TicketsPage.class, milestoneParameters));
- Label label = new Label("progress");
- WicketUtils.setCssStyle(label, "width:" + progress + "%;");
- milestoneProgress.add(label);
- WicketUtils.setHtmlTooltip(milestoneProgress, MessageFormat.format("{0} open, {1} closed", open, closed));
- add(milestoneProgress);
- }
-
-
- /*
- * TICKET DESCRIPTION (DISCUSSION TAB)
- */
- String desc;
- if (StringUtils.isEmpty(ticket.body)) {
- desc = getString("gb.noDescriptionGiven");
- } else {
- desc = MarkdownUtils.transformGFM(app().settings(), ticket.body, ticket.repository);
- }
- add(new Label("ticketDescription", desc).setEscapeModelStrings(false));
-
-
- /*
- * PARTICIPANTS (DISCUSSION TAB)
- */
- if (app().settings().getBoolean(Keys.web.allowGravatar, true)) {
- // gravatar allowed
- List<String> participants = ticket.getParticipants();
- add(new Label("participantsLabel", MessageFormat.format(getString(participants.size() > 1 ? "gb.nParticipants" : "gb.oneParticipant"),
- "<b>" + participants.size() + "</b>")).setEscapeModelStrings(false));
- ListDataProvider<String> participantsDp = new ListDataProvider<String>(participants);
- DataView<String> participantsView = new DataView<String>("participants", participantsDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<String> item) {
- String username = item.getModelObject();
- UserModel user = app().users().getUserModel(username);
- if (user == null) {
- user = new UserModel(username);
- }
- item.add(new GravatarImage("participant", user.getDisplayName(),
- user.emailAddress, null, 25, true));
- }
- };
- add(participantsView);
- } else {
- // gravatar prohibited
- add(new Label("participantsLabel").setVisible(false));
- add(new Label("participants").setVisible(false));
- }
-
-
- /*
- * LARGE STATUS INDICATOR WITH ICON (DISCUSSION TAB->SIDE BAR)
- */
- Fragment ticketStatus = new Fragment("ticketStatus", "ticketStatusFragment", this);
- Label ticketIcon = getStateIcon("ticketIcon", ticket);
- ticketStatus.add(ticketIcon);
- ticketStatus.add(new Label("ticketStatus", ticket.status.toString()));
- WicketUtils.setCssClass(ticketStatus, getLozengeClass(ticket.status, false));
- add(ticketStatus);
-
-
- /*
- * UPDATE FORM (DISCUSSION TAB)
- */
- if (isAuthenticated && app().tickets().isAcceptingTicketUpdates(repository)) {
- Fragment controls = new Fragment("controls", "controlsFragment", this);
-
-
- /*
- * STATUS
- */
- List<Status> choices = new ArrayList<Status>();
- if (ticket.isProposal()) {
- choices.addAll(Arrays.asList(TicketModel.Status.proposalWorkflow));
- } else if (ticket.isBug()) {
- choices.addAll(Arrays.asList(TicketModel.Status.bugWorkflow));
- } else {
- choices.addAll(Arrays.asList(TicketModel.Status.requestWorkflow));
- }
- choices.remove(ticket.status);
-
- ListDataProvider<Status> workflowDp = new ListDataProvider<Status>(choices);
- DataView<Status> statusView = new DataView<Status>("newStatus", workflowDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<Status> item) {
- SimpleAjaxLink<Status> link = new SimpleAjaxLink<Status>("link", item.getModel()) {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- public void onClick(AjaxRequestTarget target) {
- Status status = getModel().getObject();
- Change change = new Change(user.username);
- change.setField(Field.status, status);
- if (!ticket.isWatching(user.username)) {
- change.watch(user.username);
- }
- TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
- app().tickets().createNotifier().sendMailing(update);
- setResponsePage(TicketsPage.class, getPageParameters());
- }
- };
- String css = getStatusClass(item.getModel().getObject());
- WicketUtils.setCssClass(link, css);
- item.add(link);
- }
- };
- controls.add(statusView);
-
- /*
- * RESPONSIBLE LIST
- */
- Set<String> userlist = new TreeSet<String>(ticket.getParticipants());
- for (RegistrantAccessPermission rp : app().repositories().getUserAccessPermissions(getRepositoryModel())) {
- if (rp.permission.atLeast(AccessPermission.PUSH) && !rp.isTeam()) {
- userlist.add(rp.registrant);
- }
- }
- List<TicketResponsible> responsibles = new ArrayList<TicketResponsible>();
- if (!StringUtils.isEmpty(ticket.responsible)) {
- // exclude the current responsible
- userlist.remove(ticket.responsible);
- }
- for (String username : userlist) {
- UserModel u = app().users().getUserModel(username);
- if (u != null) {
- responsibles.add(new TicketResponsible(u));
- }
- }
- Collections.sort(responsibles);
- responsibles.add(new TicketResponsible(ESC_NIL, "", ""));
- ListDataProvider<TicketResponsible> responsibleDp = new ListDataProvider<TicketResponsible>(responsibles);
- DataView<TicketResponsible> responsibleView = new DataView<TicketResponsible>("newResponsible", responsibleDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<TicketResponsible> item) {
- SimpleAjaxLink<TicketResponsible> link = new SimpleAjaxLink<TicketResponsible>("link", item.getModel()) {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- public void onClick(AjaxRequestTarget target) {
- TicketResponsible responsible = getModel().getObject();
- Change change = new Change(user.username);
- change.setField(Field.responsible, responsible.username);
- if (!StringUtils.isEmpty(responsible.username)) {
- if (!ticket.isWatching(responsible.username)) {
- change.watch(responsible.username);
- }
- }
- if (!ticket.isWatching(user.username)) {
- change.watch(user.username);
- }
- TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
- app().tickets().createNotifier().sendMailing(update);
- setResponsePage(TicketsPage.class, getPageParameters());
- }
- };
- item.add(link);
- }
- };
- controls.add(responsibleView);
-
- /*
- * MILESTONE LIST
- */
- List<TicketMilestone> milestones = app().tickets().getMilestones(repository, Status.Open);
- if (!StringUtils.isEmpty(ticket.milestone)) {
- for (TicketMilestone milestone : milestones) {
- if (milestone.name.equals(ticket.milestone)) {
- milestones.remove(milestone);
- break;
- }
- }
- }
- milestones.add(new TicketMilestone(ESC_NIL));
- ListDataProvider<TicketMilestone> milestoneDp = new ListDataProvider<TicketMilestone>(milestones);
- DataView<TicketMilestone> milestoneView = new DataView<TicketMilestone>("newMilestone", milestoneDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<TicketMilestone> item) {
- SimpleAjaxLink<TicketMilestone> link = new SimpleAjaxLink<TicketMilestone>("link", item.getModel()) {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- public void onClick(AjaxRequestTarget target) {
- TicketMilestone milestone = getModel().getObject();
- Change change = new Change(user.username);
- if (NIL.equals(milestone.name)) {
- change.setField(Field.milestone, "");
- } else {
- change.setField(Field.milestone, milestone.name);
- }
- if (!ticket.isWatching(user.username)) {
- change.watch(user.username);
- }
- TicketModel update = app().tickets().updateTicket(repository, ticket.number, change);
- app().tickets().createNotifier().sendMailing(update);
- setResponsePage(TicketsPage.class, getPageParameters());
- }
- };
- item.add(link);
- }
- };
- controls.add(milestoneView);
-
- String editHref = urlFor(EditTicketPage.class, params).toString();
- controls.add(new ExternalLink("editLink", editHref, getString("gb.edit")));
-
- add(controls);
- } else {
- add(new Label("controls").setVisible(false));
- }
-
-
- /*
- * TICKET METADATA
- */
- add(new Label("ticketType", ticket.type.toString()));
- if (StringUtils.isEmpty(ticket.topic)) {
- add(new Label("ticketTopic").setVisible(false));
- } else {
- // process the topic using the bugtraq config to link things
- String topic = messageProcessor().processPlainCommitMessage(getRepository(), repositoryName, ticket.topic);
- add(new Label("ticketTopic", topic).setEscapeModelStrings(false));
- }
-
-
- /*
- * VOTERS
- */
- List<String> voters = ticket.getVoters();
- Label votersCount = new Label("votes", "" + voters.size());
- if (voters.size() == 0) {
- WicketUtils.setCssClass(votersCount, "badge");
- } else {
- WicketUtils.setCssClass(votersCount, "badge badge-info");
- }
- add(votersCount);
- if (user.isAuthenticated) {
- Model<String> model;
- if (ticket.isVoter(user.username)) {
- model = Model.of(getString("gb.removeVote"));
- } else {
- model = Model.of(MessageFormat.format(getString("gb.vote"), ticket.type.toString()));
- }
- SimpleAjaxLink<String> link = new SimpleAjaxLink<String>("voteLink", model) {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- public void onClick(AjaxRequestTarget target) {
- Change change = new Change(user.username);
- if (ticket.isVoter(user.username)) {
- change.unvote(user.username);
- } else {
- change.vote(user.username);
- }
- app().tickets().updateTicket(repository, ticket.number, change);
- setResponsePage(TicketsPage.class, getPageParameters());
- }
- };
- add(link);
- } else {
- add(new Label("voteLink").setVisible(false));
- }
-
-
- /*
- * WATCHERS
- */
- List<String> watchers = ticket.getWatchers();
- Label watchersCount = new Label("watchers", "" + watchers.size());
- if (watchers.size() == 0) {
- WicketUtils.setCssClass(watchersCount, "badge");
- } else {
- WicketUtils.setCssClass(watchersCount, "badge badge-info");
- }
- add(watchersCount);
- if (user.isAuthenticated) {
- Model<String> model;
- if (ticket.isWatching(user.username)) {
- model = Model.of(getString("gb.stopWatching"));
- } else {
- model = Model.of(MessageFormat.format(getString("gb.watch"), ticket.type.toString()));
- }
- SimpleAjaxLink<String> link = new SimpleAjaxLink<String>("watchLink", model) {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- public void onClick(AjaxRequestTarget target) {
- Change change = new Change(user.username);
- if (ticket.isWatching(user.username)) {
- change.unwatch(user.username);
- } else {
- change.watch(user.username);
- }
- app().tickets().updateTicket(repository, ticket.number, change);
- setResponsePage(TicketsPage.class, getPageParameters());
- }
- };
- add(link);
- } else {
- add(new Label("watchLink").setVisible(false));
- }
-
-
- /*
- * TOPIC & LABELS (DISCUSSION TAB->SIDE BAR)
- */
- ListDataProvider<String> labelsDp = new ListDataProvider<String>(ticket.getLabels());
- DataView<String> labelsView = new DataView<String>("labels", labelsDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<String> item) {
- final String value = item.getModelObject();
- Label label = new Label("label", value);
- TicketLabel tLabel = app().tickets().getLabel(repository, value);
- String background = MessageFormat.format("background-color:{0};", tLabel.color);
- label.add(new SimpleAttributeModifier("style", background));
- item.add(label);
- }
- };
-
- add(labelsView);
-
-
- /*
- * COMMENTS & STATUS CHANGES (DISCUSSION TAB)
- */
- if (comments.size() == 0) {
- add(new Label("discussion").setVisible(false));
- } else {
- Fragment discussionFragment = new Fragment("discussion", "discussionFragment", this);
- ListDataProvider<Change> discussionDp = new ListDataProvider<Change>(discussion);
- DataView<Change> discussionView = new DataView<Change>("discussion", discussionDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<Change> item) {
- final Change entry = item.getModelObject();
- if (entry.isMerge()) {
- /*
- * MERGE
- */
- String resolvedBy = entry.getString(Field.mergeSha);
-
- // identify the merged patch, it is likely the last
- Patchset mergedPatch = null;
- for (Change c : revisions) {
- if (c.patchset.tip.equals(resolvedBy)) {
- mergedPatch = c.patchset;
- break;
- }
- }
-
- String commitLink;
- if (mergedPatch == null) {
- // shouldn't happen, but just-in-case
- int len = app().settings().getInteger(Keys.web.shortCommitIdLength, 6);
- commitLink = resolvedBy.substring(0, len);
- } else {
- // expected result
- commitLink = mergedPatch.toString();
- }
-
- Fragment mergeFragment = new Fragment("entry", "mergeFragment", this);
- mergeFragment.add(new LinkPanel("commitLink", null, commitLink,
- CommitPage.class, WicketUtils.newObjectParameter(repositoryName, resolvedBy)));
- mergeFragment.add(new Label("toBranch", MessageFormat.format(getString("gb.toBranch"),
- "<b>" + ticket.mergeTo + "</b>")).setEscapeModelStrings(false));
- addUserAttributions(mergeFragment, entry, 0);
- addDateAttributions(mergeFragment, entry);
-
- item.add(mergeFragment);
- } else if (entry.isStatusChange()) {
- /*
- * STATUS CHANGE
- */
- Fragment frag = new Fragment("entry", "statusFragment", this);
- Label status = new Label("statusChange", entry.getStatus().toString());
- String css = getLozengeClass(entry.getStatus(), false);
- WicketUtils.setCssClass(status, css);
- for (IBehavior b : status.getBehaviors()) {
- if (b instanceof SimpleAttributeModifier) {
- SimpleAttributeModifier sam = (SimpleAttributeModifier) b;
- if ("class".equals(sam.getAttribute())) {
- status.add(new SimpleAttributeModifier("class", "status-change " + sam.getValue()));
- break;
- }
- }
- }
- frag.add(status);
- addUserAttributions(frag, entry, avatarWidth);
- addDateAttributions(frag, entry);
- item.add(frag);
- } else {
- /*
- * COMMENT
- */
- String comment = MarkdownUtils.transformGFM(app().settings(), entry.comment.text, repositoryName);
- Fragment frag = new Fragment("entry", "commentFragment", this);
- Label commentIcon = new Label("commentIcon");
- if (entry.comment.src == CommentSource.Email) {
- WicketUtils.setCssClass(commentIcon, "iconic-mail");
- } else {
- WicketUtils.setCssClass(commentIcon, "iconic-comment-alt2-stroke");
- }
- frag.add(commentIcon);
- frag.add(new Label("comment", comment).setEscapeModelStrings(false));
- addUserAttributions(frag, entry, avatarWidth);
- addDateAttributions(frag, entry);
- item.add(frag);
- }
- }
- };
- discussionFragment.add(discussionView);
- add(discussionFragment);
- }
-
- /*
- * ADD COMMENT PANEL
- */
- if (UserModel.ANONYMOUS.equals(user)
- || !repository.isBare
- || repository.isFrozen
- || repository.isMirror) {
-
- // prohibit comments for anonymous users, local working copy repos,
- // frozen repos, and mirrors
- add(new Label("newComment").setVisible(false));
- } else {
- // permit user to comment
- Fragment newComment = new Fragment("newComment", "newCommentFragment", this);
- GravatarImage img = new GravatarImage("newCommentAvatar", user.username, user.emailAddress,
- "gravatar-round", avatarWidth, true);
- newComment.add(img);
- CommentPanel commentPanel = new CommentPanel("commentPanel", user, ticket, null, TicketsPage.class);
- commentPanel.setRepository(repositoryName);
- newComment.add(commentPanel);
- add(newComment);
- }
-
-
- /*
- * PATCHSET TAB
- */
- if (currentPatchset == null) {
- // no patchset yet, show propose fragment
- String repoUrl = getRepositoryUrl(user, repository);
- Fragment changeIdFrag = new Fragment("patchset", "proposeFragment", this);
- changeIdFrag.add(new Label("proposeInstructions", MarkdownUtils.transformMarkdown(getString("gb.proposeInstructions"))).setEscapeModelStrings(false));
- changeIdFrag.add(new Label("ptWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Barnum")));
- changeIdFrag.add(new Label("ptWorkflowSteps", getProposeWorkflow("propose_pt.md", repoUrl, ticket.number)).setEscapeModelStrings(false));
- changeIdFrag.add(new Label("gitWorkflow", MessageFormat.format(getString("gb.proposeWith"), "Git")));
- changeIdFrag.add(new Label("gitWorkflowSteps", getProposeWorkflow("propose_git.md", repoUrl, ticket.number)).setEscapeModelStrings(false));
- add(changeIdFrag);
- } else {
- // show current patchset
- Fragment patchsetFrag = new Fragment("patchset", "patchsetFragment", this);
- patchsetFrag.add(new Label("commitsInPatchset", MessageFormat.format(getString("gb.commitsInPatchsetN"), currentPatchset.number)));
-
- // current revision
- MarkupContainer panel = createPatchsetPanel("panel", repository, user);
- patchsetFrag.add(panel);
- addUserAttributions(patchsetFrag, currentRevision, avatarWidth);
- addUserAttributions(panel, currentRevision, 0);
- addDateAttributions(panel, currentRevision);
-
- // commits
- List<RevCommit> commits = JGitUtils.getRevLog(getRepository(), currentPatchset.base, currentPatchset.tip);
- ListDataProvider<RevCommit> commitsDp = new ListDataProvider<RevCommit>(commits);
- DataView<RevCommit> commitsView = new DataView<RevCommit>("commit", commitsDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<RevCommit> item) {
- RevCommit commit = item.getModelObject();
- PersonIdent author = commit.getAuthorIdent();
- item.add(new GravatarImage("authorAvatar", author.getName(), author.getEmailAddress(), null, 16, false));
- item.add(new Label("author", commit.getAuthorIdent().getName()));
- item.add(new LinkPanel("commitId", null, getShortObjectId(commit.getName()),
- CommitPage.class, WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
- item.add(new LinkPanel("diff", "link", getString("gb.diff"), CommitDiffPage.class,
- WicketUtils.newObjectParameter(repositoryName, commit.getName()), true));
- item.add(new Label("title", StringUtils.trimString(commit.getShortMessage(), Constants.LEN_SHORTLOG_REFS)));
- item.add(WicketUtils.createDateLabel("commitDate", JGitUtils.getCommitDate(commit), GitBlitWebSession
- .get().getTimezone(), getTimeUtils(), false));
- item.add(new DiffStatPanel("commitDiffStat", 0, 0, true));
- }
- };
- patchsetFrag.add(commitsView);
- add(patchsetFrag);
- }
-
-
- /*
- * ACTIVITY TAB
- */
- Fragment revisionHistory = new Fragment("activity", "activityFragment", this);
- List<Change> events = new ArrayList<Change>(ticket.changes);
- Collections.sort(events);
- Collections.reverse(events);
- ListDataProvider<Change> eventsDp = new ListDataProvider<Change>(events);
- DataView<Change> eventsView = new DataView<Change>("event", eventsDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<Change> item) {
- Change event = item.getModelObject();
-
- addUserAttributions(item, event, 16);
-
- if (event.hasPatchset()) {
- // patchset
- Patchset patchset = event.patchset;
- String what;
- if (event.isStatusChange() && (Status.New == event.getStatus())) {
- what = getString("gb.proposedThisChange");
- } else if (patchset.rev == 1) {
- what = MessageFormat.format(getString("gb.uploadedPatchsetN"), patchset.number);
- } else {
- if (patchset.added == 1) {
- what = getString("gb.addedOneCommit");
- } else {
- what = MessageFormat.format(getString("gb.addedNCommits"), patchset.added);
- }
- }
- item.add(new Label("what", what));
-
- LinkPanel psr = new LinkPanel("patchsetRevision", null, patchset.number + "-" + patchset.rev,
- ComparePage.class, WicketUtils.newRangeParameter(repositoryName, patchset.parent == null ? patchset.base : patchset.parent, patchset.tip), true);
- WicketUtils.setHtmlTooltip(psr, patchset.toString());
- item.add(psr);
- String typeCss = getPatchsetTypeCss(patchset.type);
- Label typeLabel = new Label("patchsetType", patchset.type.toString());
- if (typeCss == null) {
- typeLabel.setVisible(false);
- } else {
- WicketUtils.setCssClass(typeLabel, typeCss);
- }
- item.add(typeLabel);
-
- // show commit diffstat
- item.add(new DiffStatPanel("patchsetDiffStat", patchset.insertions, patchset.deletions, patchset.rev > 1));
- } else if (event.hasComment()) {
- // comment
- item.add(new Label("what", getString("gb.commented")));
- item.add(new Label("patchsetRevision").setVisible(false));
- item.add(new Label("patchsetType").setVisible(false));
- item.add(new Label("patchsetDiffStat").setVisible(false));
- } else if (event.hasReview()) {
- // review
- String score;
- switch (event.review.score) {
- case approved:
- score = "<span style='color:darkGreen'>" + getScoreDescription(event.review.score) + "</span>";
- break;
- case vetoed:
- score = "<span style='color:darkRed'>" + getScoreDescription(event.review.score) + "</span>";
- break;
- default:
- score = getScoreDescription(event.review.score);
- }
- item.add(new Label("what", MessageFormat.format(getString("gb.reviewedPatchsetRev"),
- event.review.patchset, event.review.rev, score))
- .setEscapeModelStrings(false));
- item.add(new Label("patchsetRevision").setVisible(false));
- item.add(new Label("patchsetType").setVisible(false));
- item.add(new Label("patchsetDiffStat").setVisible(false));
- } else {
- // field change
- item.add(new Label("patchsetRevision").setVisible(false));
- item.add(new Label("patchsetType").setVisible(false));
- item.add(new Label("patchsetDiffStat").setVisible(false));
-
- String what = "";
- if (event.isStatusChange()) {
- switch (event.getStatus()) {
- case New:
- if (ticket.isProposal()) {
- what = getString("gb.proposedThisChange");
- } else {
- what = getString("gb.createdThisTicket");
- }
- break;
- default:
- break;
- }
- }
- item.add(new Label("what", what).setVisible(what.length() > 0));
- }
-
- addDateAttributions(item, event);
-
- if (event.hasFieldChanges()) {
- StringBuilder sb = new StringBuilder();
- sb.append("<table class=\"summary\"><tbody>");
- for (Map.Entry<Field, String> entry : event.fields.entrySet()) {
- String value;
- switch (entry.getKey()) {
- case body:
- String body = entry.getValue();
- if (event.isStatusChange() && Status.New == event.getStatus() && StringUtils.isEmpty(body)) {
- // ignore initial empty description
- continue;
- }
- // trim body changes
- if (StringUtils.isEmpty(body)) {
- value = "<i>" + ESC_NIL + "</i>";
- } else {
- value = StringUtils.trimString(body, Constants.LEN_SHORTLOG_REFS);
- }
- break;
- case status:
- // special handling for status
- Status status = event.getStatus();
- String css = getLozengeClass(status, true);
- value = String.format("<span class=\"%1$s\">%2$s</span>", css, status.toString());
- break;
- default:
- value = StringUtils.isEmpty(entry.getValue()) ? ("<i>" + ESC_NIL + "</i>") : StringUtils.escapeForHtml(entry.getValue(), false);
- break;
- }
- sb.append("<tr><th style=\"width:70px;\">");
- sb.append(entry.getKey().name());
- sb.append("</th><td>");
- sb.append(value);
- sb.append("</td></tr>");
- }
- sb.append("</tbody></table>");
- item.add(new Label("fields", sb.toString()).setEscapeModelStrings(false));
- } else {
- item.add(new Label("fields").setVisible(false));
- }
- }
- };
- revisionHistory.add(eventsView);
- add(revisionHistory);
- }
-
- protected void addUserAttributions(MarkupContainer container, Change entry, int avatarSize) {
- UserModel commenter = app().users().getUserModel(entry.author);
- if (commenter == null) {
- // unknown user
- container.add(new GravatarImage("changeAvatar", entry.author,
- entry.author, null, avatarSize, false).setVisible(avatarSize > 0));
- container.add(new Label("changeAuthor", entry.author.toLowerCase()));
- } else {
- // known user
- container.add(new GravatarImage("changeAvatar", commenter.getDisplayName(),
- commenter.emailAddress, avatarSize > 24 ? "gravatar-round" : null,
- avatarSize, true).setVisible(avatarSize > 0));
- container.add(new LinkPanel("changeAuthor", null, commenter.getDisplayName(),
- UserPage.class, WicketUtils.newUsernameParameter(commenter.username)));
- }
- }
-
- protected void addDateAttributions(MarkupContainer container, Change entry) {
- container.add(WicketUtils.createDateLabel("changeDate", entry.date, GitBlitWebSession
- .get().getTimezone(), getTimeUtils(), false));
-
- // set the id attribute
- if (entry.hasComment()) {
- container.setOutputMarkupId(true);
- container.add(new AttributeModifier("id", Model.of(entry.getId())));
- ExternalLink link = new ExternalLink("changeLink", "#" + entry.getId());
- container.add(link);
- } else {
- container.add(new Label("changeLink").setVisible(false));
- }
- }
-
- protected String getProposeWorkflow(String resource, String url, long ticketId) {
- String md = readResource(resource);
- md = md.replace("${url}", url);
- md = md.replace("${repo}", StringUtils.getLastPathElement(StringUtils.stripDotGit(repositoryName)));
- md = md.replace("${ticketId}", "" + ticketId);
- md = md.replace("${patchset}", "" + 1);
- md = md.replace("${reviewBranch}", Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticketId)));
- md = md.replace("${integrationBranch}", Repository.shortenRefName(getRepositoryModel().HEAD));
- return MarkdownUtils.transformMarkdown(md);
- }
-
- protected Fragment createPatchsetPanel(String wicketId, RepositoryModel repository, UserModel user) {
- final Patchset currentPatchset = ticket.getCurrentPatchset();
- List<Patchset> patchsets = new ArrayList<Patchset>(ticket.getPatchsetRevisions(currentPatchset.number));
- patchsets.remove(currentPatchset);
- Collections.reverse(patchsets);
-
- Fragment panel = new Fragment(wicketId, "collapsiblePatchsetFragment", this);
-
- // patchset header
- String ps = "<b>" + currentPatchset.number + "</b>";
- if (currentPatchset.rev == 1) {
- panel.add(new Label("uploadedWhat", MessageFormat.format(getString("gb.uploadedPatchsetN"), ps)).setEscapeModelStrings(false));
- } else {
- String rev = "<b>" + currentPatchset.rev + "</b>";
- panel.add(new Label("uploadedWhat", MessageFormat.format(getString("gb.uploadedPatchsetNRevisionN"), ps, rev)).setEscapeModelStrings(false));
- }
- panel.add(new LinkPanel("patchId", null, "rev " + currentPatchset.rev,
- CommitPage.class, WicketUtils.newObjectParameter(repositoryName, currentPatchset.tip), true));
-
- // compare menu
- panel.add(new LinkPanel("compareMergeBase", null, getString("gb.compareToMergeBase"),
- ComparePage.class, WicketUtils.newRangeParameter(repositoryName, currentPatchset.base, currentPatchset.tip), true));
-
- ListDataProvider<Patchset> compareMenuDp = new ListDataProvider<Patchset>(patchsets);
- DataView<Patchset> compareMenu = new DataView<Patchset>("comparePatch", compareMenuDp) {
- private static final long serialVersionUID = 1L;
- @Override
- public void populateItem(final Item<Patchset> item) {
- Patchset patchset = item.getModelObject();
- LinkPanel link = new LinkPanel("compareLink", null,
- MessageFormat.format(getString("gb.compareToN"), patchset.number + "-" + patchset.rev),
- ComparePage.class, WicketUtils.newRangeParameter(getRepositoryModel().name,
- patchset.tip, currentPatchset.tip), true);
- item.add(link);
-
- }
- };
- panel.add(compareMenu);
-
-
- // reviews
- List<Change> reviews = ticket.getReviews(currentPatchset);
- ListDataProvider<Change> reviewsDp = new ListDataProvider<Change>(reviews);
- DataView<Change> reviewsView = new DataView<Change>("reviews", reviewsDp) {
- private static final long serialVersionUID = 1L;
-
- @Override
- public void populateItem(final Item<Change> item) {
- Change change = item.getModelObject();
- final String username = change.author;
- UserModel user = app().users().getUserModel(username);
- if (user == null) {
- item.add(new Label("reviewer", username));
- } else {
- item.add(new LinkPanel("reviewer", null, user.getDisplayName(),
- UserPage.class, WicketUtils.newUsernameParameter(username)));
- }
-
- // indicate review score
- Review review = change.review;
- Label scoreLabel = new Label("score");
- String scoreClass = getScoreClass(review.score);
- String tooltip = getScoreDescription(review.score);
- WicketUtils.setCssClass(scoreLabel, scoreClass);
- if (!StringUtils.isEmpty(tooltip)) {
- WicketUtils.setHtmlTooltip(scoreLabel, tooltip);
- }
- item.add(scoreLabel);
- }
- };
- panel.add(reviewsView);
-
-
- if (ticket.isOpen() && user.canReviewPatchset(repository)) {
- // can only review open tickets
- Review myReview = null;
- for (Change change : ticket.getReviews(currentPatchset)) {
- if (change.author.equals(user.username)) {
- myReview = change.review;
- }
- }
-
- // user can review, add review controls
- Fragment reviewControls = new Fragment("reviewControls", "reviewControlsFragment", this);
-
- // show "approve" button if no review OR not current score
- if (user.canApprovePatchset(repository) && (myReview == null || Score.approved != myReview.score)) {
- reviewControls.add(createReviewLink("approveLink", Score.approved));
- } else {
- reviewControls.add(new Label("approveLink").setVisible(false));
- }
-
- // show "looks good" button if no review OR not current score
- if (myReview == null || Score.looks_good != myReview.score) {
- reviewControls.add(createReviewLink("looksGoodLink", Score.looks_good));
- } else {
- reviewControls.add(new Label("looksGoodLink").setVisible(false));
- }
-
- // show "needs improvement" button if no review OR not current score
- if (myReview == null || Score.needs_improvement != myReview.score) {
- reviewControls.add(createReviewLink("needsImprovementLink", Score.needs_improvement));
- } else {
- reviewControls.add(new Label("needsImprovementLink").setVisible(false));
- }
-
- // show "veto" button if no review OR not current score
- if (user.canVetoPatchset(repository) && (myReview == null || Score.vetoed != myReview.score)) {
- reviewControls.add(createReviewLink("vetoLink", Score.vetoed));
- } else {
- reviewControls.add(new Label("vetoLink").setVisible(false));
- }
- panel.add(reviewControls);
- } else {
- // user can not review
- panel.add(new Label("reviewControls").setVisible(false));
- }
-
- String insertions = MessageFormat.format("<span style=\"color:darkGreen;font-weight:bold;\">+{0}</span>", ticket.insertions);
- String deletions = MessageFormat.format("<span style=\"color:darkRed;font-weight:bold;\">-{0}</span>", ticket.deletions);
- panel.add(new Label("patchsetStat", MessageFormat.format(StringUtils.escapeForHtml(getString("gb.diffStat"), false),
- insertions, deletions)).setEscapeModelStrings(false));
-
- // changed paths list
- List<PathChangeModel> paths = JGitUtils.getFilesInRange(getRepository(), currentPatchset.base, currentPatchset.tip);
- ListDataProvider<PathChangeModel> pathsDp = new ListDataProvider<PathChangeModel>(paths);
- DataView<PathChangeModel> pathsView = new DataView<PathChangeModel>("changedPath", pathsDp) {
- private static final long serialVersionUID = 1L;
- int counter;
-
- @Override
- public void populateItem(final Item<PathChangeModel> item) {
- final PathChangeModel entry = item.getModelObject();
- Label changeType = new Label("changeType", "");
- WicketUtils.setChangeTypeCssClass(changeType, entry.changeType);
- setChangeTypeTooltip(changeType, entry.changeType);
- item.add(changeType);
- item.add(new DiffStatPanel("diffStat", entry.insertions, entry.deletions, true));
-
- boolean hasSubmodule = false;
- String submodulePath = null;
- if (entry.isTree()) {
- // tree
- item.add(new LinkPanel("pathName", null, entry.path, TreePage.class,
- WicketUtils
- .newPathParameter(repositoryName, currentPatchset.tip, entry.path), true));
- item.add(new Label("diffStat").setVisible(false));
- } else if (entry.isSubmodule()) {
- // submodule
- String submoduleId = entry.objectId;
- SubmoduleModel submodule = getSubmodule(entry.path);
- submodulePath = submodule.gitblitPath;
- hasSubmodule = submodule.hasSubmodule;
-
- item.add(new LinkPanel("pathName", "list", entry.path + " @ " +
- getShortObjectId(submoduleId), TreePage.class,
- WicketUtils.newPathParameter(submodulePath, submoduleId, ""), true).setEnabled(hasSubmodule));
- item.add(new Label("diffStat").setVisible(false));
- } else {
- // blob
- String displayPath = entry.path;
- String path = entry.path;
- if (entry.isSymlink()) {
- RevCommit commit = JGitUtils.getCommit(getRepository(), Constants.R_TICKETS_PATCHSETS + ticket.number);
- path = JGitUtils.getStringContent(getRepository(), commit.getTree(), path);
- displayPath = entry.path + " -> " + path;
- }
-
- if (entry.changeType.equals(ChangeType.ADD)) {
- // add show view
- item.add(new LinkPanel("pathName", "list", displayPath, BlobPage.class,
- WicketUtils.newPathParameter(repositoryName, currentPatchset.tip, path), true));
- } else if (entry.changeType.equals(ChangeType.DELETE)) {
- // delete, show label
- item.add(new Label("pathName", displayPath));
- } else {
- // mod, show diff
- item.add(new LinkPanel("pathName", "list", displayPath, BlobDiffPage.class,
- WicketUtils.newPathParameter(repositoryName, currentPatchset.tip, path), true));
- }
- }
-
- // quick links
- if (entry.isSubmodule()) {
- // submodule
- item.add(setNewTarget(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
- .newPathParameter(repositoryName, entry.commitId, entry.path)))
- .setEnabled(!entry.changeType.equals(ChangeType.ADD)));
- item.add(new BookmarkablePageLink<Void>("view", CommitPage.class, WicketUtils
- .newObjectParameter(submodulePath, entry.objectId)).setEnabled(hasSubmodule));
- } else {
- // tree or blob
- item.add(setNewTarget(new BookmarkablePageLink<Void>("diff", BlobDiffPage.class, WicketUtils
- .newBlobDiffParameter(repositoryName, currentPatchset.base, currentPatchset.tip, entry.path)))
- .setEnabled(!entry.changeType.equals(ChangeType.ADD)
- && !entry.changeType.equals(ChangeType.DELETE)));
- item.add(setNewTarget(new BookmarkablePageLink<Void>("view", BlobPage.class, WicketUtils
- .newPathParameter(repositoryName, currentPatchset.tip, entry.path)))
- .setEnabled(!entry.changeType.equals(ChangeType.DELETE)));
- }
-
- WicketUtils.setAlternatingBackground(item, counter);
- counter++;
- }
- };
- panel.add(pathsView);
-
- addPtReviewInstructions(user, repository, panel);
- addGitReviewInstructions(user, repository, panel);
- panel.add(createMergePanel(user, repository));
-
- return panel;
- }
-
- protected IconAjaxLink<String> createReviewLink(String wicketId, final Score score) {
- return new IconAjaxLink<String>(wicketId, getScoreClass(score), Model.of(getScoreDescription(score))) {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- public void onClick(AjaxRequestTarget target) {
- review(score);
- }
- };
- }
-
- protected String getScoreClass(Score score) {
- switch (score) {
- case vetoed:
- return "fa fa-exclamation-circle";
- case needs_improvement:
- return "fa fa-thumbs-o-down";
- case looks_good:
- return "fa fa-thumbs-o-up";
- case approved:
- return "fa fa-check-circle";
- case not_reviewed:
- default:
- return "fa fa-minus-circle";
- }
- }
-
- protected String getScoreDescription(Score score) {
- String description;
- switch (score) {
- case vetoed:
- description = getString("gb.veto");
- break;
- case needs_improvement:
- description = getString("gb.needsImprovement");
- break;
- case looks_good:
- description = getString("gb.looksGood");
- break;
- case approved:
- description = getString("gb.approve");
- break;
- case not_reviewed:
- default:
- description = getString("gb.hasNotReviewed");
- }
- return String.format("%1$s (%2$+d)", description, score.getValue());
- }
-
- protected void review(Score score) {
- UserModel user = GitBlitWebSession.get().getUser();
- Patchset ps = ticket.getCurrentPatchset();
- Change change = new Change(user.username);
- change.review(ps, score, !ticket.isReviewer(user.username));
- if (!ticket.isWatching(user.username)) {
- change.watch(user.username);
- }
- TicketModel updatedTicket = app().tickets().updateTicket(getRepositoryModel(), ticket.number, change);
- app().tickets().createNotifier().sendMailing(updatedTicket);
- setResponsePage(TicketsPage.class, getPageParameters());
- }
-
- protected <X extends MarkupContainer> X setNewTarget(X x) {
- x.add(new SimpleAttributeModifier("target", "_blank"));
- return x;
- }
-
- protected void addGitReviewInstructions(UserModel user, RepositoryModel repository, MarkupContainer panel) {
- String repoUrl = getRepositoryUrl(user, repository);
-
- panel.add(new Label("gitStep1", MessageFormat.format(getString("gb.stepN"), 1)));
- panel.add(new Label("gitStep2", MessageFormat.format(getString("gb.stepN"), 2)));
-
- String ticketBranch = Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticket.number));
- String reviewBranch = PatchsetCommand.getReviewBranch(ticket.number);
-
- String step1 = MessageFormat.format("git fetch {0} {1}", repoUrl, ticketBranch);
- String step2 = MessageFormat.format("git checkout -B {0} FETCH_HEAD", reviewBranch);
-
- panel.add(new Label("gitPreStep1", step1));
- panel.add(new Label("gitPreStep2", step2));
-
- panel.add(createCopyFragment("gitCopyStep1", step1.replace("\n", " && ")));
- panel.add(createCopyFragment("gitCopyStep2", step2.replace("\n", " && ")));
- }
-
- protected void addPtReviewInstructions(UserModel user, RepositoryModel repository, MarkupContainer panel) {
- String step1 = MessageFormat.format("pt checkout {0,number,0}", ticket.number);
- panel.add(new Label("ptPreStep", step1));
- panel.add(createCopyFragment("ptCopyStep", step1));
- }
-
- /**
- * Adds a merge panel for the patchset to the markup container. The panel
- * may just a message if the patchset can not be merged.
- *
- * @param c
- * @param user
- * @param repository
- */
- protected Component createMergePanel(UserModel user, RepositoryModel repository) {
- Patchset patchset = ticket.getCurrentPatchset();
- if (patchset == null) {
- // no patchset to merge
- return new Label("mergePanel");
- }
-
- boolean allowMerge;
- if (repository.requireApproval) {
- // rpeository requires approval
- allowMerge = ticket.isOpen() && ticket.isApproved(patchset);
- } else {
- // vetos are binding
- allowMerge = ticket.isOpen() && !ticket.isVetoed(patchset);
- }
-
- MergeStatus mergeStatus = JGitUtils.canMerge(getRepository(), patchset.tip, ticket.mergeTo);
- if (allowMerge) {
- if (MergeStatus.MERGEABLE == mergeStatus) {
- // patchset can be cleanly merged to integration branch OR has already been merged
- Fragment mergePanel = new Fragment("mergePanel", "mergeableFragment", this);
- mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetMergeable"), ticket.mergeTo)));
- if (user.canPush(repository)) {
- // user can merge locally
- SimpleAjaxLink<String> mergeButton = new SimpleAjaxLink<String>("mergeButton", Model.of(getString("gb.merge"))) {
-
- private static final long serialVersionUID = 1L;
-
- @Override
- public void onClick(AjaxRequestTarget target) {
-
- // ensure the patchset is still current AND not vetoed
- Patchset patchset = ticket.getCurrentPatchset();
- final TicketModel refreshedTicket = app().tickets().getTicket(getRepositoryModel(), ticket.number);
- if (patchset.equals(refreshedTicket.getCurrentPatchset())) {
- // patchset is current, check for recent veto
- if (!refreshedTicket.isVetoed(patchset)) {
- // patchset is not vetoed
-
- // execute the merge using the ticket service
- app().tickets().exec(new Runnable() {
- @Override
- public void run() {
- PatchsetReceivePack rp = new PatchsetReceivePack(
- app().gitblit(),
- getRepository(),
- getRepositoryModel(),
- GitBlitWebSession.get().getUser());
- MergeStatus result = rp.merge(refreshedTicket);
- if (MergeStatus.MERGED == result) {
- // notify participants and watchers
- rp.sendAll();
- } else {
- // merge failure
- String msg = MessageFormat.format("Failed to merge ticket {0,number,0}: {1}", ticket.number, result.name());
- logger.error(msg);
- GitBlitWebSession.get().cacheErrorMessage(msg);
- }
- }
- });
- } else {
- // vetoed patchset
- String msg = MessageFormat.format("Can not merge ticket {0,number,0}, patchset {1,number,0} has been vetoed!",
- ticket.number, patchset.number);
- GitBlitWebSession.get().cacheErrorMessage(msg);
- logger.error(msg);
- }
- } else {
- // not current patchset
- String msg = MessageFormat.format("Can not merge ticket {0,number,0}, the patchset has been updated!", ticket.number);
- GitBlitWebSession.get().cacheErrorMessage(msg);
- logger.error(msg);
- }
-
- setResponsePage(TicketsPage.class, getPageParameters());
- }
- };
- mergePanel.add(mergeButton);
- Component instructions = getMergeInstructions(user, repository, "mergeMore", "gb.patchsetMergeableMore");
- mergePanel.add(instructions);
- } else {
- mergePanel.add(new Label("mergeButton").setVisible(false));
- mergePanel.add(new Label("mergeMore").setVisible(false));
- }
- return mergePanel;
- } else if (MergeStatus.ALREADY_MERGED == mergeStatus) {
- // patchset already merged
- Fragment mergePanel = new Fragment("mergePanel", "alreadyMergedFragment", this);
- mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetAlreadyMerged"), ticket.mergeTo)));
- return mergePanel;
- } else {
- // patchset can not be cleanly merged
- Fragment mergePanel = new Fragment("mergePanel", "notMergeableFragment", this);
- mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
- if (user.canPush(repository)) {
- // user can merge locally
- Component instructions = getMergeInstructions(user, repository, "mergeMore", "gb.patchsetNotMergeableMore");
- mergePanel.add(instructions);
- } else {
- mergePanel.add(new Label("mergeMore").setVisible(false));
- }
- return mergePanel;
- }
- } else {
- // merge not allowed
- if (MergeStatus.ALREADY_MERGED == mergeStatus) {
- // patchset already merged
- Fragment mergePanel = new Fragment("mergePanel", "alreadyMergedFragment", this);
- mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetAlreadyMerged"), ticket.mergeTo)));
- return mergePanel;
- } else if (ticket.isVetoed(patchset)) {
- // patchset has been vetoed
- Fragment mergePanel = new Fragment("mergePanel", "vetoedFragment", this);
- mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotMergeable"), ticket.mergeTo)));
- return mergePanel;
- } else if (repository.requireApproval) {
- // patchset has been not been approved for merge
- Fragment mergePanel = new Fragment("mergePanel", "notApprovedFragment", this);
- mergePanel.add(new Label("mergeTitle", MessageFormat.format(getString("gb.patchsetNotApproved"), ticket.mergeTo)));
- mergePanel.add(new Label("mergeMore", MessageFormat.format(getString("gb.patchsetNotApprovedMore"), ticket.mergeTo)));
- return mergePanel;
- } else {
- // other case
- return new Label("mergePanel");
- }
- }
- }
-
- protected Component getMergeInstructions(UserModel user, RepositoryModel repository, String markupId, String infoKey) {
- Fragment cmd = new Fragment(markupId, "commandlineMergeFragment", this);
- cmd.add(new Label("instructions", MessageFormat.format(getString(infoKey), ticket.mergeTo)));
- String repoUrl = getRepositoryUrl(user, repository);
-
- // git instructions
- cmd.add(new Label("mergeStep1", MessageFormat.format(getString("gb.stepN"), 1)));
- cmd.add(new Label("mergeStep2", MessageFormat.format(getString("gb.stepN"), 2)));
- cmd.add(new Label("mergeStep3", MessageFormat.format(getString("gb.stepN"), 3)));
-
- String ticketBranch = Repository.shortenRefName(PatchsetCommand.getTicketBranch(ticket.number));
- String reviewBranch = PatchsetCommand.getReviewBranch(ticket.number);
-
- String step1 = MessageFormat.format("git checkout -B {0} {1}", reviewBranch, ticket.mergeTo);
- String step2 = MessageFormat.format("git pull {0} {1}", repoUrl, ticketBranch);
- String step3 = MessageFormat.format("git checkout {0}\ngit merge {1}\ngit push origin {0}", ticket.mergeTo, reviewBranch);
-
- cmd.add(new Label("mergePreStep1", step1));
- cmd.add(new Label("mergePreStep2", step2));
- cmd.add(new Label("mergePreStep3", step3));
-
- cmd.add(createCopyFragment("mergeCopyStep1", step1.replace("\n", " && ")));
- cmd.add(createCopyFragment("mergeCopyStep2", step2.replace("\n", " && ")));
- cmd.add(createCopyFragment("mergeCopyStep3", step3.replace("\n", " && ")));
-
- // pt instructions
- String ptStep = MessageFormat.format("pt pull {0,number,0}", ticket.number);
- cmd.add(new Label("ptMergeStep", ptStep));
- cmd.add(createCopyFragment("ptMergeCopyStep", step1.replace("\n", " && ")));
- return cmd;
- }
-
- /**
- * Returns the primary repository url
- *
- * @param user
- * @param repository
- * @return the primary repository url
- */
- protected String getRepositoryUrl(UserModel user, RepositoryModel repository) {
- HttpServletRequest req = ((WebRequest) getRequest()).getHttpServletRequest();
- String primaryurl = app().gitblit().getRepositoryUrls(req, user, repository).get(0).url;
- String url = primaryurl;
- try {
- url = new URIish(primaryurl).setUser(null).toString();
- } catch (Exception e) {
- }
- return url;
- }
-
- /**
- * Returns the ticket (if any) that this commit references.
- *
- * @param commit
- * @return null or a ticket
- */
- protected TicketModel getTicket(RevCommit commit) {
- try {
- Map<String, Ref> refs = getRepository().getRefDatabase().getRefs(Constants.R_TICKETS_PATCHSETS);
- for (Map.Entry<String, Ref> entry : refs.entrySet()) {
- if (entry.getValue().getObjectId().equals(commit.getId())) {
- long id = PatchsetCommand.getTicketNumber(entry.getKey());
- TicketModel ticket = app().tickets().getTicket(getRepositoryModel(), id);
- return ticket;
- }
- }
- } catch (Exception e) {
- logger().error("failed to determine ticket from ref", e);
- }
- return null;
- }
-
- protected String getPatchsetTypeCss(PatchsetType type) {
- String typeCss;
- switch (type) {
- case Rebase:
- case Rebase_Squash:
- typeCss = getLozengeClass(Status.Declined, false);
- break;
- case Squash:
- case Amend:
- typeCss = getLozengeClass(Status.On_Hold, false);
- break;
- case Proposal:
- typeCss = getLozengeClass(Status.New, false);
- break;
- case FastForward:
- default:
- typeCss = null;
- break;
- }
- return typeCss;
- }
-
- @Override
- protected String getPageName() {
- return getString("gb.ticket");
- }
-
- @Override
- protected Class<? extends BasePage> getRepoNavPageClass() {
- return TicketsPage.class;
- }
-
- @Override
- protected String getPageTitle(String repositoryName) {
- return "#" + ticket.number + " - " + ticket.title;
- }
-
- protected Fragment createCopyFragment(String wicketId, String text) {
- if (app().settings().getBoolean(Keys.web.allowFlashCopyToClipboard, true)) {
- // clippy: flash-based copy & paste
- Fragment copyFragment = new Fragment(wicketId, "clippyPanel", this);
- String baseUrl = WicketUtils.getGitblitURL(getRequest());
- ShockWaveComponent clippy = new ShockWaveComponent("clippy", baseUrl + "/clippy.swf");
- clippy.setValue("flashVars", "text=" + StringUtils.encodeURL(text));
- copyFragment.add(clippy);
- return copyFragment;
- } else {
- // javascript: manual copy & paste with modal browser prompt dialog
- Fragment copyFragment = new Fragment(wicketId, "jsPanel", this);
- ContextImage img = WicketUtils.newImage("copyIcon", "clippy.png");
- img.add(new JavascriptTextPrompt("onclick", "Copy to Clipboard (Ctrl+C, Enter)", text));
- copyFragment.add(img);
- return copyFragment;
- }
- }
- }
|