diff --git a/src/main/java/com/gitblit/models/TicketModel.java b/src/main/java/com/gitblit/models/TicketModel.java index fd0b09eb9..f8813f01c 100644 --- a/src/main/java/com/gitblit/models/TicketModel.java +++ b/src/main/java/com/gitblit/models/TicketModel.java @@ -233,6 +233,10 @@ public boolean hasLabel(String label) { public List getLabels() { return getList(Field.labels); } + + public List getDependencies() { + return getList(Field.dependency); + } public boolean isResponsible(String username) { return username.equals(responsible); @@ -746,6 +750,26 @@ public void setField(Field field, Object value) { fields.put(field, value.toString()); } } + + public void setDeltaField(Field field, List base, List newValues) { + List result = new ArrayList<>(); + for (String oldValue : base) { + if (!newValues.contains(oldValue)) { + result.add("-" + oldValue); + } + } + for (String newValue : newValues) { + if (!base.contains(newValue)) { + result.add("+" + newValue); + } + } + if (result.isEmpty()) { + // no change + remove(field); + } else { + setField(field, join(result, ",")); + } + } public void remove(Field field) { if (fields != null) { @@ -1195,7 +1219,7 @@ public static Score fromScore(int score) { public static enum Field { title, body, responsible, type, status, milestone, mergeSha, mergeTo, - topic, labels, watchers, reviewers, voters, mentions, priority, severity; + topic, labels, watchers, reviewers, voters, mentions, priority, severity, dependency; } public static enum Type { diff --git a/src/main/java/com/gitblit/tickets/QueryResult.java b/src/main/java/com/gitblit/tickets/QueryResult.java index f8d6d1232..14698c3d9 100644 --- a/src/main/java/com/gitblit/tickets/QueryResult.java +++ b/src/main/java/com/gitblit/tickets/QueryResult.java @@ -60,6 +60,7 @@ public class QueryResult implements Serializable { public List participants; public List watchedby; public List mentions; + public List dependencies; public Patchset patchset; public int commentsCount; public int votesCount; diff --git a/src/main/java/com/gitblit/tickets/TicketIndexer.java b/src/main/java/com/gitblit/tickets/TicketIndexer.java index e2d53af7b..5db5590d1 100644 --- a/src/main/java/com/gitblit/tickets/TicketIndexer.java +++ b/src/main/java/com/gitblit/tickets/TicketIndexer.java @@ -20,6 +20,7 @@ import java.text.MessageFormat; import java.text.ParseException; import java.util.ArrayList; +import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.LinkedHashSet; @@ -106,7 +107,8 @@ public static enum Lucene { votes(Type.INT), //NOTE: Indexing on the underlying value to allow flexibility on naming priority(Type.INT), - severity(Type.INT); + severity(Type.INT), + dependencies(Type.STRING); final Type fieldType; @@ -521,6 +523,9 @@ private Document ticketToDoc(TicketModel ticket) { toDocField(doc, Lucene.participants, StringUtils.flattenStrings(ticket.getParticipants(), ";").toLowerCase()); toDocField(doc, Lucene.watchedby, StringUtils.flattenStrings(ticket.getWatchers(), ";").toLowerCase()); toDocField(doc, Lucene.mentions, StringUtils.flattenStrings(ticket.getMentions(), ";").toLowerCase()); + for (String dep : ticket.getDependencies()) { + toDocField(doc, Lucene.dependencies, dep); + } toDocField(doc, Lucene.votes, ticket.getVoters().size()); toDocField(doc, Lucene.priority, ticket.priority.getValue()); toDocField(doc, Lucene.severity, ticket.severity.getValue()); @@ -607,6 +612,7 @@ private QueryResult docToQueryResult(Document doc) throws ParseException { result.mentions = unpackStrings(doc, Lucene.mentions); result.priority = TicketModel.Priority.fromObject(unpackInt(doc, Lucene.priority), TicketModel.Priority.defaultPriority); result.severity = TicketModel.Severity.fromObject(unpackInt(doc, Lucene.severity), TicketModel.Severity.defaultSeverity); + result.dependencies = convertList(doc, Lucene.dependencies); if (!StringUtils.isEmpty(doc.get(Lucene.patchset.name()))) { // unpack most recent patchset @@ -625,6 +631,10 @@ private QueryResult docToQueryResult(Document doc) throws ParseException { return result; } + private List convertList(Document doc, Lucene lucene) { + return Arrays.asList(doc.getValues(lucene.name())); + } + private String unpackString(Document doc, Lucene lucene) { return doc.get(lucene.name()); } diff --git a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties index 648ac2a54..98b619a47 100644 --- a/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties +++ b/src/main/java/com/gitblit/wicket/GitBlitWebApp.properties @@ -513,6 +513,11 @@ gb.docsWelcome1 = You can use docs to document your repository. gb.docsWelcome2 = Commit a README.md or a HOME.md file to get started. gb.createReadme = create a README gb.responsible = responsible +gb.dependency = dependency +gb.dependencies = dependencies +gb.remove = remove +gb.dependant = dependant +gb.showDependant = show dependant gb.createdThisTicket = created this ticket gb.proposedThisChange = proposed this change gb.uploadedPatchsetN = uploaded patchset {0} @@ -759,4 +764,4 @@ gb.diffCopiedFile = File was copied from {0} gb.diffTruncated = Diff truncated after the above file gb.opacityAdjust = Adjust opacity gb.blinkComparator = Blink comparator -gb.imgdiffSubtract = Subtract (black = identical) \ No newline at end of file +gb.imgdiffSubtract = Subtract (black = identical) diff --git a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.html b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.html index b12d0c773..764adc50d 100644 --- a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.html +++ b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.html @@ -43,6 +43,7 @@ + diff --git a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java index 192b48caa..6b0906299 100644 --- a/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/EditTicketPage.java @@ -30,15 +30,19 @@ import org.apache.wicket.markup.html.form.DropDownChoice; import org.apache.wicket.markup.html.form.Form; import org.apache.wicket.markup.html.form.TextField; +import org.apache.wicket.markup.html.list.ListItem; +import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.model.IModel; import org.apache.wicket.model.Model; import org.eclipse.jgit.lib.Repository; +import org.jsoup.helper.StringUtil; import com.gitblit.Constants; import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.AuthorizationControl; import com.gitblit.models.RegistrantAccessPermission; +import com.gitblit.models.RepositoryModel; import com.gitblit.models.TicketModel; import com.gitblit.models.TicketModel.Change; import com.gitblit.models.TicketModel.Field; @@ -51,7 +55,10 @@ import com.gitblit.utils.StringUtils; import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.WicketUtils; +import com.gitblit.wicket.panels.LinkPanel; import com.gitblit.wicket.panels.MarkdownTextArea; +import com.gitblit.wicket.panels.SimpleAjaxLink; +import com.gitblit.wicket.panels.TicketRelationEditorPanel; import com.google.common.base.Optional; /** @@ -81,6 +88,8 @@ public class EditTicketPage extends RepositoryPage { private IModel responsibleModel; private IModel milestoneModel; + + private IModel> dependenciesModel; private Label descriptionPreview; @@ -123,6 +132,7 @@ public EditTicketPage(PageParameters params) { statusModel = Model.of(ticket.status); priorityModel = Model.of(ticket.priority); severityModel = Model.of(ticket.severity); + dependenciesModel = Model.ofList((List) editable(ticket.getDependencies())); setStatelessHint(false); setOutputMarkupId(true); @@ -141,6 +151,16 @@ public EditTicketPage(PageParameters params) { form.add(new TextField("title", titleModel)); form.add(new TextField("topic", topicModel)); + form.setOutputMarkupId(true); + + form.add(new TicketRelationEditorPanel("dependencies", dependenciesModel, ticket.number) { + private static final long serialVersionUID = 1L; + @Override + protected RepositoryModel getRepositoryModel() { + return EditTicketPage.this.getRepositoryModel(); + } + }); + final IModel markdownPreviewModel = Model.of(ticket.body == null ? "" : ticket.body); descriptionPreview = new Label("descriptionPreview", markdownPreviewModel); descriptionPreview.setEscapeModelStrings(false); @@ -311,6 +331,10 @@ protected void onSubmit(AjaxRequestTarget target, Form form) { change.setField(Field.topic, topic); } + List newDependencies = (List) dependenciesModel.getObject(); + + change.setDeltaField(Field.dependency, ticket.getDependencies(), newDependencies); + TicketResponsible responsible = responsibleModel == null ? null : responsibleModel.getObject(); if (responsible != null && !responsible.username.equals(ticket.responsible)) { // responsible change @@ -382,6 +406,11 @@ public void onSubmit() { form.add(cancel); } + private List editable(List list) { + // need to copy, if it's an Collection.emptyList + return new ArrayList(list); + } + @Override protected String getPageName() { return getString("gb.editTicket"); diff --git a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.html b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.html index 9b5af0238..d0b7378d9 100644 --- a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.html +++ b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.html @@ -43,6 +43,7 @@ + diff --git a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java index 0c52505c6..011f4b68b 100644 --- a/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/NewTicketPage.java @@ -39,6 +39,7 @@ import com.gitblit.Constants.AccessPermission; import com.gitblit.Constants.AuthorizationControl; import com.gitblit.models.RegistrantAccessPermission; +import com.gitblit.models.RepositoryModel; import com.gitblit.models.TicketModel; import com.gitblit.models.TicketModel.Change; import com.gitblit.models.TicketModel.Field; @@ -51,6 +52,7 @@ import com.gitblit.wicket.GitBlitWebSession; import com.gitblit.wicket.WicketUtils; import com.gitblit.wicket.panels.MarkdownTextArea; +import com.gitblit.wicket.panels.TicketRelationEditorPanel; /** * Page for creating a new ticket. @@ -80,6 +82,8 @@ public class NewTicketPage extends RepositoryPage { private IModel severityModel; + private IModel> dependenciesModel; + public NewTicketPage(PageParameters params) { super(params); @@ -101,6 +105,7 @@ public NewTicketPage(PageParameters params) { milestoneModel = Model.of(); severityModel = Model.of(TicketModel.Severity.defaultSeverity); priorityModel = Model.of(TicketModel.Priority.defaultPriority); + dependenciesModel = (IModel) Model.ofList(new ArrayList()); setStatelessHint(false); setOutputMarkupId(true); @@ -122,6 +127,16 @@ public NewTicketPage(PageParameters params) { descriptionEditor = new MarkdownTextArea("description", markdownPreviewModel, descriptionPreview); descriptionEditor.setRepository(repositoryName); form.add(descriptionEditor); + + form.add(new TicketRelationEditorPanel("dependencies", dependenciesModel, null) { + + private static final long serialVersionUID = 1L; + + @Override + protected RepositoryModel getRepositoryModel() { + return NewTicketPage.this.getRepositoryModel(); + } + }); if (currentUser.canAdmin(null, getRepositoryModel())) { // responsible @@ -244,6 +259,8 @@ protected void onSubmit(AjaxRequestTarget target, Form form) { if (!StringUtils.isEmpty(mergeTo)) { change.setField(Field.mergeTo, mergeTo); } + + change.setDeltaField(Field.dependency, Collections.emptyList(), dependenciesModel.getObject()); TicketModel ticket = app().tickets().createTicket(getRepositoryModel(), 0L, change); if (ticket != null) { diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.html b/src/main/java/com/gitblit/wicket/pages/TicketPage.html index 5ae005ebb..26f3f03fe 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketPage.html +++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.html @@ -72,6 +72,8 @@ [topic] [responsible] [milestone] + [link] + 1 vote 1 watch diff --git a/src/main/java/com/gitblit/wicket/pages/TicketPage.java b/src/main/java/com/gitblit/wicket/pages/TicketPage.java index 4890874ac..ae4819626 100644 --- a/src/main/java/com/gitblit/wicket/pages/TicketPage.java +++ b/src/main/java/com/gitblit/wicket/pages/TicketPage.java @@ -42,6 +42,8 @@ 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.list.ListItem; +import org.apache.wicket.markup.html.list.ListView; import org.apache.wicket.markup.html.panel.Fragment; import org.apache.wicket.markup.repeater.Item; import org.apache.wicket.markup.repeater.data.DataView; @@ -292,6 +294,20 @@ public TicketPage(PageParameters params) { } add(new Label("ticketDescription", desc).setEscapeModelStrings(false)); + /* + * DEPENDENCY + */ + List dependencies = ticket.getDependencies(); + add(new ListView("dependencies", dependencies) { + @Override + protected void populateItem(ListItem item) { + String ticketId= item.getModelObject(); + PageParameters tp = WicketUtils.newObjectParameter(ticket.repository, ticketId); + item.add(new LinkPanel("dependencyLink", "list subject", "#"+ticketId, TicketsPage.class, tp)); + } + }); + + add(new BookmarkablePageLink("dependOnLink", TicketsPage.class, queryDependsOn(ticket.number))); /* * PARTICIPANTS (DISCUSSION TAB) @@ -983,6 +999,12 @@ public void populateItem(final Item item) { add(revisionHistory); } + private PageParameters queryDependsOn(long number) { + PageParameters params = WicketUtils.newRepositoryParameter(repositoryName); + params.add("q", Lucene.dependencies.name() + ':' + number); + return params; + } + protected void addUserAttributions(MarkupContainer container, Change entry, int avatarSize) { UserModel commenter = app().users().getUserModel(entry.author); if (commenter == null) { diff --git a/src/main/java/com/gitblit/wicket/panels/TicketRelationEditorPanel.html b/src/main/java/com/gitblit/wicket/panels/TicketRelationEditorPanel.html new file mode 100644 index 000000000..79c381fb5 --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/TicketRelationEditorPanel.html @@ -0,0 +1,28 @@ + + + + + + + + + + + [ticket label] + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/java/com/gitblit/wicket/panels/TicketRelationEditorPanel.java b/src/main/java/com/gitblit/wicket/panels/TicketRelationEditorPanel.java new file mode 100644 index 000000000..9b905c01d --- /dev/null +++ b/src/main/java/com/gitblit/wicket/panels/TicketRelationEditorPanel.java @@ -0,0 +1,115 @@ +package com.gitblit.wicket.panels; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import org.apache.wicket.PageParameters; +import org.apache.wicket.ajax.AjaxRequestTarget; +import org.apache.wicket.ajax.markup.html.form.AjaxButton; +import org.apache.wicket.markup.html.form.Form; +import org.apache.wicket.markup.html.form.TextField; +import org.apache.wicket.markup.html.list.ListItem; +import org.apache.wicket.markup.html.list.ListView; +import org.apache.wicket.model.IModel; +import org.apache.wicket.model.Model; +import org.jsoup.helper.StringUtil; + +import com.gitblit.models.RepositoryModel; +import com.gitblit.models.TicketModel; +import com.gitblit.tickets.ITicketService; +import com.gitblit.wicket.WicketUtils; +import com.gitblit.wicket.pages.TicketsPage; + +public abstract class TicketRelationEditorPanel extends BasePanel { + + private static final long serialVersionUID = 1L; + + private IModel> dependenciesModel; + private IModel addDependencyModel; + private Long baseTicketId; + + public TicketRelationEditorPanel(String wicketId, IModel> pdependenciesModel, Long baseTicketId) { + super(wicketId); + this.dependenciesModel = pdependenciesModel; + this.addDependencyModel = Model.of(); + this.baseTicketId = baseTicketId; + + + add(new ListView("dependencyList", dependenciesModel) { + private static final long serialVersionUID = 1L; + + @Override + protected void populateItem(ListItem item) { + final String ticketId = item.getModelObject(); + + PageParameters tp = WicketUtils.newObjectParameter(getRepositoryModel().name, ticketId); + item.add(new LinkPanel("dependencyLink", "list subject", "#"+ticketId, TicketsPage.class, tp)); + + item.add(new AjaxButton("removeDependencyLink") { + private static final long serialVersionUID = 1L; + @Override + public void onSubmit(AjaxRequestTarget target, Form form) { + List list = dependenciesModel.getObject(); + list.remove(ticketId); + dependenciesModel.setObject(list); + target.addComponent(form); + } + }); + } + }); + add(new TextField("addDependencyText", addDependencyModel)); + add(new AjaxButton("addDependency") { + private static final long serialVersionUID = 1L; + @Override + protected void onSubmit(AjaxRequestTarget target, Form form) { + String ticketIdStr = addDependencyModel.getObject(); + if (!StringUtil.isBlank(ticketIdStr)) { + if (checkCycle(ticketIdStr)) { + List list = (List) dependenciesModel.getObject(); + list.add(ticketIdStr.trim()); + addDependencyModel.setObject(""); + } + } + target.addComponent(form); + } + }); + } + + private boolean checkCycle(String ticketId) { + Set tickets = new HashSet(); + if (baseTicketId != null) { + tickets.add(baseTicketId); + } + return checkCycle(tickets, ticketId); + } + + private boolean checkCycle(Set tickets, String ticketIdStr) { + try { + long ticketId = Long.parseLong(ticketIdStr); + if (tickets.contains(ticketId)) { + return false; + } + ITicketService ticketService = app().tickets(); + RepositoryModel r = getRepositoryModel(); + if (ticketService.hasTicket(r, ticketId)) { + TicketModel ticket = ticketService.getTicket(r, ticketId); + tickets.add(ticketId); + for (String subTicketIdStr : ticket.getDependencies()) { + if (!checkCycle(tickets, subTicketIdStr)) { + return false; + } + } + tickets.remove(ticketId); + return true; + } else { + return false; + } + } catch (NumberFormatException e) { + return false; + } + } + + protected abstract RepositoryModel getRepositoryModel(); + +}