/*
|
* 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.models;
|
|
import java.io.ByteArrayInputStream;
|
import java.io.ByteArrayOutputStream;
|
import java.io.IOException;
|
import java.io.ObjectInputStream;
|
import java.io.ObjectOutputStream;
|
import java.io.Serializable;
|
import java.io.UnsupportedEncodingException;
|
import java.security.MessageDigest;
|
import java.security.NoSuchAlgorithmException;
|
import java.text.MessageFormat;
|
import java.util.ArrayList;
|
import java.util.Arrays;
|
import java.util.Collection;
|
import java.util.Collections;
|
import java.util.Date;
|
import java.util.HashMap;
|
import java.util.LinkedHashMap;
|
import java.util.LinkedHashSet;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.NoSuchElementException;
|
import java.util.Set;
|
import java.util.TreeSet;
|
import java.util.regex.Matcher;
|
import java.util.regex.Pattern;
|
|
import org.eclipse.jgit.util.RelativeDateFormatter;
|
|
/**
|
* The Gitblit Ticket model, its component classes, and enums.
|
*
|
* @author James Moger
|
*
|
*/
|
public class TicketModel implements Serializable, Comparable<TicketModel> {
|
|
private static final long serialVersionUID = 1L;
|
|
public String project;
|
|
public String repository;
|
|
public long number;
|
|
public Date created;
|
|
public String createdBy;
|
|
public Date updated;
|
|
public String updatedBy;
|
|
public String title;
|
|
public String body;
|
|
public String topic;
|
|
public Type type;
|
|
public Status status;
|
|
public String responsible;
|
|
public String milestone;
|
|
public String mergeSha;
|
|
public String mergeTo;
|
|
public List<Change> changes;
|
|
public Integer insertions;
|
|
public Integer deletions;
|
|
/**
|
* Builds an effective ticket from the collection of changes. A change may
|
* Add or Subtract information from a ticket, but the collection of changes
|
* is only additive.
|
*
|
* @param changes
|
* @return the effective ticket
|
*/
|
public static TicketModel buildTicket(Collection<Change> changes) {
|
TicketModel ticket;
|
List<Change> effectiveChanges = new ArrayList<Change>();
|
Map<String, Change> comments = new HashMap<String, Change>();
|
for (Change change : changes) {
|
if (change.comment != null) {
|
if (comments.containsKey(change.comment.id)) {
|
Change original = comments.get(change.comment.id);
|
Change clone = copy(original);
|
clone.comment.text = change.comment.text;
|
clone.comment.deleted = change.comment.deleted;
|
int idx = effectiveChanges.indexOf(original);
|
effectiveChanges.remove(original);
|
effectiveChanges.add(idx, clone);
|
comments.put(clone.comment.id, clone);
|
} else {
|
effectiveChanges.add(change);
|
comments.put(change.comment.id, change);
|
}
|
} else {
|
effectiveChanges.add(change);
|
}
|
}
|
|
// effective ticket
|
ticket = new TicketModel();
|
for (Change change : effectiveChanges) {
|
if (!change.hasComment()) {
|
// ensure we do not include a deleted comment
|
change.comment = null;
|
}
|
ticket.applyChange(change);
|
}
|
return ticket;
|
}
|
|
public TicketModel() {
|
// the first applied change set the date appropriately
|
created = new Date(0);
|
changes = new ArrayList<Change>();
|
status = Status.New;
|
type = Type.defaultType;
|
}
|
|
public boolean isOpen() {
|
return !status.isClosed();
|
}
|
|
public boolean isClosed() {
|
return status.isClosed();
|
}
|
|
public boolean isMerged() {
|
return isClosed() && !isEmpty(mergeSha);
|
}
|
|
public boolean isProposal() {
|
return Type.Proposal == type;
|
}
|
|
public boolean isBug() {
|
return Type.Bug == type;
|
}
|
|
public Date getLastUpdated() {
|
return updated == null ? created : updated;
|
}
|
|
public boolean hasPatchsets() {
|
return getPatchsets().size() > 0;
|
}
|
|
/**
|
* Returns true if multiple participants are involved in discussing a ticket.
|
* The ticket creator is excluded from this determination because a
|
* discussion requires more than one participant.
|
*
|
* @return true if this ticket has a discussion
|
*/
|
public boolean hasDiscussion() {
|
for (Change change : getComments()) {
|
if (!change.author.equals(createdBy)) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
/**
|
* Returns the list of changes with comments.
|
*
|
* @return
|
*/
|
public List<Change> getComments() {
|
List<Change> list = new ArrayList<Change>();
|
for (Change change : changes) {
|
if (change.hasComment()) {
|
list.add(change);
|
}
|
}
|
return list;
|
}
|
|
/**
|
* Returns the list of participants for the ticket.
|
*
|
* @return the list of participants
|
*/
|
public List<String> getParticipants() {
|
Set<String> set = new LinkedHashSet<String>();
|
for (Change change : changes) {
|
if (change.isParticipantChange()) {
|
set.add(change.author);
|
}
|
}
|
if (responsible != null && responsible.length() > 0) {
|
set.add(responsible);
|
}
|
return new ArrayList<String>(set);
|
}
|
|
public boolean hasLabel(String label) {
|
return getLabels().contains(label);
|
}
|
|
public List<String> getLabels() {
|
return getList(Field.labels);
|
}
|
|
public boolean isResponsible(String username) {
|
return username.equals(responsible);
|
}
|
|
public boolean isAuthor(String username) {
|
return username.equals(createdBy);
|
}
|
|
public boolean isReviewer(String username) {
|
return getReviewers().contains(username);
|
}
|
|
public List<String> getReviewers() {
|
return getList(Field.reviewers);
|
}
|
|
public boolean isWatching(String username) {
|
return getWatchers().contains(username);
|
}
|
|
public List<String> getWatchers() {
|
return getList(Field.watchers);
|
}
|
|
public boolean isVoter(String username) {
|
return getVoters().contains(username);
|
}
|
|
public List<String> getVoters() {
|
return getList(Field.voters);
|
}
|
|
public List<String> getMentions() {
|
return getList(Field.mentions);
|
}
|
|
protected List<String> getList(Field field) {
|
Set<String> set = new TreeSet<String>();
|
for (Change change : changes) {
|
if (change.hasField(field)) {
|
String values = change.getString(field);
|
for (String value : values.split(",")) {
|
switch (value.charAt(0)) {
|
case '+':
|
set.add(value.substring(1));
|
break;
|
case '-':
|
set.remove(value.substring(1));
|
break;
|
default:
|
set.add(value);
|
}
|
}
|
}
|
}
|
if (!set.isEmpty()) {
|
return new ArrayList<String>(set);
|
}
|
return Collections.emptyList();
|
}
|
|
public Attachment getAttachment(String name) {
|
Attachment attachment = null;
|
for (Change change : changes) {
|
if (change.hasAttachments()) {
|
Attachment a = change.getAttachment(name);
|
if (a != null) {
|
attachment = a;
|
}
|
}
|
}
|
return attachment;
|
}
|
|
public boolean hasAttachments() {
|
for (Change change : changes) {
|
if (change.hasAttachments()) {
|
return true;
|
}
|
}
|
return false;
|
}
|
|
public List<Attachment> getAttachments() {
|
List<Attachment> list = new ArrayList<Attachment>();
|
for (Change change : changes) {
|
if (change.hasAttachments()) {
|
list.addAll(change.attachments);
|
}
|
}
|
return list;
|
}
|
|
public List<Patchset> getPatchsets() {
|
List<Patchset> list = new ArrayList<Patchset>();
|
for (Change change : changes) {
|
if (change.patchset != null) {
|
list.add(change.patchset);
|
}
|
}
|
return list;
|
}
|
|
public List<Patchset> getPatchsetRevisions(int number) {
|
List<Patchset> list = new ArrayList<Patchset>();
|
for (Change change : changes) {
|
if (change.patchset != null) {
|
if (number == change.patchset.number) {
|
list.add(change.patchset);
|
}
|
}
|
}
|
return list;
|
}
|
|
public Patchset getPatchset(String sha) {
|
for (Change change : changes) {
|
if (change.patchset != null) {
|
if (sha.equals(change.patchset.tip)) {
|
return change.patchset;
|
}
|
}
|
}
|
return null;
|
}
|
|
public Patchset getPatchset(int number, int rev) {
|
for (Change change : changes) {
|
if (change.patchset != null) {
|
if (number == change.patchset.number && rev == change.patchset.rev) {
|
return change.patchset;
|
}
|
}
|
}
|
return null;
|
}
|
|
public Patchset getCurrentPatchset() {
|
Patchset patchset = null;
|
for (Change change : changes) {
|
if (change.patchset != null) {
|
if (patchset == null) {
|
patchset = change.patchset;
|
} else if (patchset.compareTo(change.patchset) == 1) {
|
patchset = change.patchset;
|
}
|
}
|
}
|
return patchset;
|
}
|
|
public boolean isCurrent(Patchset patchset) {
|
if (patchset == null) {
|
return false;
|
}
|
Patchset curr = getCurrentPatchset();
|
if (curr == null) {
|
return false;
|
}
|
return curr.equals(patchset);
|
}
|
|
public List<Change> getReviews(Patchset patchset) {
|
if (patchset == null) {
|
return Collections.emptyList();
|
}
|
// collect the patchset reviews by author
|
// the last review by the author is the
|
// official review
|
Map<String, Change> reviews = new LinkedHashMap<String, TicketModel.Change>();
|
for (Change change : changes) {
|
if (change.hasReview()) {
|
if (change.review.isReviewOf(patchset)) {
|
reviews.put(change.author, change);
|
}
|
}
|
}
|
return new ArrayList<Change>(reviews.values());
|
}
|
|
|
public boolean isApproved(Patchset patchset) {
|
if (patchset == null) {
|
return false;
|
}
|
boolean approved = false;
|
boolean vetoed = false;
|
for (Change change : getReviews(patchset)) {
|
if (change.hasReview()) {
|
if (change.review.isReviewOf(patchset)) {
|
if (Score.approved == change.review.score) {
|
approved = true;
|
} else if (Score.vetoed == change.review.score) {
|
vetoed = true;
|
}
|
}
|
}
|
}
|
return approved && !vetoed;
|
}
|
|
public boolean isVetoed(Patchset patchset) {
|
if (patchset == null) {
|
return false;
|
}
|
for (Change change : getReviews(patchset)) {
|
if (change.hasReview()) {
|
if (change.review.isReviewOf(patchset)) {
|
if (Score.vetoed == change.review.score) {
|
return true;
|
}
|
}
|
}
|
}
|
return false;
|
}
|
|
public Review getReviewBy(String username) {
|
for (Change change : getReviews(getCurrentPatchset())) {
|
if (change.author.equals(username)) {
|
return change.review;
|
}
|
}
|
return null;
|
}
|
|
public boolean isPatchsetAuthor(String username) {
|
for (Change change : changes) {
|
if (change.hasPatchset()) {
|
if (change.author.equals(username)) {
|
return true;
|
}
|
}
|
}
|
return false;
|
}
|
|
public void applyChange(Change change) {
|
if (changes.size() == 0) {
|
// first change created the ticket
|
created = change.date;
|
createdBy = change.author;
|
status = Status.New;
|
} else if (created == null || change.date.after(created)) {
|
// track last ticket update
|
updated = change.date;
|
updatedBy = change.author;
|
}
|
|
if (change.isMerge()) {
|
// identify merge patchsets
|
if (isEmpty(responsible)) {
|
responsible = change.author;
|
}
|
status = Status.Merged;
|
}
|
|
if (change.hasFieldChanges()) {
|
for (Map.Entry<Field, String> entry : change.fields.entrySet()) {
|
Field field = entry.getKey();
|
Object value = entry.getValue();
|
switch (field) {
|
case type:
|
type = TicketModel.Type.fromObject(value, type);
|
break;
|
case status:
|
status = TicketModel.Status.fromObject(value, status);
|
break;
|
case title:
|
title = toString(value);
|
break;
|
case body:
|
body = toString(value);
|
break;
|
case topic:
|
topic = toString(value);
|
break;
|
case responsible:
|
responsible = toString(value);
|
break;
|
case milestone:
|
milestone = toString(value);
|
break;
|
case mergeTo:
|
mergeTo = toString(value);
|
break;
|
case mergeSha:
|
mergeSha = toString(value);
|
break;
|
default:
|
// unknown
|
break;
|
}
|
}
|
}
|
|
// add the change to the ticket
|
changes.add(change);
|
}
|
|
protected String toString(Object value) {
|
if (value == null) {
|
return null;
|
}
|
return value.toString();
|
}
|
|
public String toIndexableString() {
|
StringBuilder sb = new StringBuilder();
|
if (!isEmpty(title)) {
|
sb.append(title).append('\n');
|
}
|
if (!isEmpty(body)) {
|
sb.append(body).append('\n');
|
}
|
for (Change change : changes) {
|
if (change.hasComment()) {
|
sb.append(change.comment.text);
|
sb.append('\n');
|
}
|
}
|
return sb.toString();
|
}
|
|
@Override
|
public String toString() {
|
StringBuilder sb = new StringBuilder();
|
sb.append("#");
|
sb.append(number);
|
sb.append(": " + title + "\n");
|
for (Change change : changes) {
|
sb.append(change);
|
sb.append('\n');
|
}
|
return sb.toString();
|
}
|
|
@Override
|
public int compareTo(TicketModel o) {
|
return o.created.compareTo(created);
|
}
|
|
@Override
|
public boolean equals(Object o) {
|
if (o instanceof TicketModel) {
|
return number == ((TicketModel) o).number;
|
}
|
return super.equals(o);
|
}
|
|
@Override
|
public int hashCode() {
|
return (repository + number).hashCode();
|
}
|
|
/**
|
* Encapsulates a ticket change
|
*/
|
public static class Change implements Serializable, Comparable<Change> {
|
|
private static final long serialVersionUID = 1L;
|
|
public final Date date;
|
|
public final String author;
|
|
public Comment comment;
|
|
public Map<Field, String> fields;
|
|
public Set<Attachment> attachments;
|
|
public Patchset patchset;
|
|
public Review review;
|
|
private transient String id;
|
|
public Change(String author) {
|
this(author, new Date());
|
}
|
|
public Change(String author, Date date) {
|
this.date = date;
|
this.author = author;
|
}
|
|
public boolean isStatusChange() {
|
return hasField(Field.status);
|
}
|
|
public Status getStatus() {
|
Status state = Status.fromObject(getField(Field.status), null);
|
return state;
|
}
|
|
public boolean isMerge() {
|
return hasField(Field.status) && hasField(Field.mergeSha);
|
}
|
|
public boolean hasPatchset() {
|
return patchset != null;
|
}
|
|
public boolean hasReview() {
|
return review != null;
|
}
|
|
public boolean hasComment() {
|
return comment != null && !comment.isDeleted() && comment.text != null;
|
}
|
|
public Comment comment(String text) {
|
comment = new Comment(text);
|
comment.id = TicketModel.getSHA1(date.toString() + author + text);
|
|
try {
|
Pattern mentions = Pattern.compile("\\s@([A-Za-z0-9-_]+)");
|
Matcher m = mentions.matcher(text);
|
while (m.find()) {
|
String username = m.group(1);
|
plusList(Field.mentions, username);
|
}
|
} catch (Exception e) {
|
// ignore
|
}
|
return comment;
|
}
|
|
public Review review(Patchset patchset, Score score, boolean addReviewer) {
|
if (addReviewer) {
|
plusList(Field.reviewers, author);
|
}
|
review = new Review(patchset.number, patchset.rev);
|
review.score = score;
|
return review;
|
}
|
|
public boolean hasAttachments() {
|
return !TicketModel.isEmpty(attachments);
|
}
|
|
public void addAttachment(Attachment attachment) {
|
if (attachments == null) {
|
attachments = new LinkedHashSet<Attachment>();
|
}
|
attachments.add(attachment);
|
}
|
|
public Attachment getAttachment(String name) {
|
if (attachments != null) {
|
for (Attachment attachment : attachments) {
|
if (attachment.name.equalsIgnoreCase(name)) {
|
return attachment;
|
}
|
}
|
}
|
return null;
|
}
|
|
public boolean isParticipantChange() {
|
if (hasComment()
|
|| hasReview()
|
|| hasPatchset()
|
|| hasAttachments()) {
|
return true;
|
}
|
|
if (TicketModel.isEmpty(fields)) {
|
return false;
|
}
|
|
// identify real ticket field changes
|
Map<Field, String> map = new HashMap<Field, String>(fields);
|
map.remove(Field.watchers);
|
map.remove(Field.voters);
|
return !map.isEmpty();
|
}
|
|
public boolean hasField(Field field) {
|
return !TicketModel.isEmpty(getString(field));
|
}
|
|
public boolean hasFieldChanges() {
|
return !TicketModel.isEmpty(fields);
|
}
|
|
public String getField(Field field) {
|
if (fields != null) {
|
return fields.get(field);
|
}
|
return null;
|
}
|
|
public void setField(Field field, Object value) {
|
if (fields == null) {
|
fields = new LinkedHashMap<Field, String>();
|
}
|
if (value == null) {
|
fields.put(field, null);
|
} else if (Enum.class.isAssignableFrom(value.getClass())) {
|
fields.put(field, ((Enum<?>) value).name());
|
} else {
|
fields.put(field, value.toString());
|
}
|
}
|
|
public void remove(Field field) {
|
if (fields != null) {
|
fields.remove(field);
|
}
|
}
|
|
public String getString(Field field) {
|
String value = getField(field);
|
if (value == null) {
|
return null;
|
}
|
return value;
|
}
|
|
public void watch(String... username) {
|
plusList(Field.watchers, username);
|
}
|
|
public void unwatch(String... username) {
|
minusList(Field.watchers, username);
|
}
|
|
public void vote(String... username) {
|
plusList(Field.voters, username);
|
}
|
|
public void unvote(String... username) {
|
minusList(Field.voters, username);
|
}
|
|
public void label(String... label) {
|
plusList(Field.labels, label);
|
}
|
|
public void unlabel(String... label) {
|
minusList(Field.labels, label);
|
}
|
|
protected void plusList(Field field, String... items) {
|
modList(field, "+", items);
|
}
|
|
protected void minusList(Field field, String... items) {
|
modList(field, "-", items);
|
}
|
|
private void modList(Field field, String prefix, String... items) {
|
List<String> list = new ArrayList<String>();
|
for (String item : items) {
|
list.add(prefix + item);
|
}
|
if (hasField(field)) {
|
String flat = getString(field);
|
if (isEmpty(flat)) {
|
// field is empty, use this list
|
setField(field, join(list, ","));
|
} else {
|
// merge this list into the existing field list
|
Set<String> set = new TreeSet<String>(Arrays.asList(flat.split(",")));
|
set.addAll(list);
|
setField(field, join(set, ","));
|
}
|
} else {
|
// does not have a list for this field
|
setField(field, join(list, ","));
|
}
|
}
|
|
public String getId() {
|
if (id == null) {
|
id = getSHA1(Long.toHexString(date.getTime()) + author);
|
}
|
return id;
|
}
|
|
@Override
|
public int compareTo(Change c) {
|
return date.compareTo(c.date);
|
}
|
|
@Override
|
public int hashCode() {
|
return getId().hashCode();
|
}
|
|
@Override
|
public boolean equals(Object o) {
|
if (o instanceof Change) {
|
return getId().equals(((Change) o).getId());
|
}
|
return false;
|
}
|
|
@Override
|
public String toString() {
|
StringBuilder sb = new StringBuilder();
|
sb.append(RelativeDateFormatter.format(date));
|
if (hasComment()) {
|
sb.append(" commented on by ");
|
} else if (hasPatchset()) {
|
sb.append(MessageFormat.format(" {0} uploaded by ", patchset));
|
} else {
|
sb.append(" changed by ");
|
}
|
sb.append(author).append(" - ");
|
if (hasComment()) {
|
if (comment.isDeleted()) {
|
sb.append("(deleted) ");
|
}
|
sb.append(comment.text).append(" ");
|
}
|
|
if (hasFieldChanges()) {
|
for (Map.Entry<Field, String> entry : fields.entrySet()) {
|
sb.append("\n ");
|
sb.append(entry.getKey().name());
|
sb.append(':');
|
sb.append(entry.getValue());
|
}
|
}
|
return sb.toString();
|
}
|
}
|
|
/**
|
* Returns true if the string is null or empty.
|
*
|
* @param value
|
* @return true if string is null or empty
|
*/
|
static boolean isEmpty(String value) {
|
return value == null || value.trim().length() == 0;
|
}
|
|
/**
|
* Returns true if the collection is null or empty
|
*
|
* @param collection
|
* @return
|
*/
|
static boolean isEmpty(Collection<?> collection) {
|
return collection == null || collection.size() == 0;
|
}
|
|
/**
|
* Returns true if the map is null or empty
|
*
|
* @param map
|
* @return
|
*/
|
static boolean isEmpty(Map<?, ?> map) {
|
return map == null || map.size() == 0;
|
}
|
|
/**
|
* Calculates the SHA1 of the string.
|
*
|
* @param text
|
* @return sha1 of the string
|
*/
|
static String getSHA1(String text) {
|
try {
|
byte[] bytes = text.getBytes("iso-8859-1");
|
return getSHA1(bytes);
|
} catch (UnsupportedEncodingException u) {
|
throw new RuntimeException(u);
|
}
|
}
|
|
/**
|
* Calculates the SHA1 of the byte array.
|
*
|
* @param bytes
|
* @return sha1 of the byte array
|
*/
|
static String getSHA1(byte[] bytes) {
|
try {
|
MessageDigest md = MessageDigest.getInstance("SHA-1");
|
md.update(bytes, 0, bytes.length);
|
byte[] digest = md.digest();
|
return toHex(digest);
|
} catch (NoSuchAlgorithmException t) {
|
throw new RuntimeException(t);
|
}
|
}
|
|
/**
|
* Returns the hex representation of the byte array.
|
*
|
* @param bytes
|
* @return byte array as hex string
|
*/
|
static String toHex(byte[] bytes) {
|
StringBuilder sb = new StringBuilder(bytes.length * 2);
|
for (int i = 0; i < bytes.length; i++) {
|
if ((bytes[i] & 0xff) < 0x10) {
|
sb.append('0');
|
}
|
sb.append(Long.toString(bytes[i] & 0xff, 16));
|
}
|
return sb.toString();
|
}
|
|
/**
|
* Join the list of strings into a single string with a space separator.
|
*
|
* @param values
|
* @return joined list
|
*/
|
static String join(Collection<String> values) {
|
return join(values, " ");
|
}
|
|
/**
|
* Join the list of strings into a single string with the specified
|
* separator.
|
*
|
* @param values
|
* @param separator
|
* @return joined list
|
*/
|
static String join(String[] values, String separator) {
|
return join(Arrays.asList(values), separator);
|
}
|
|
/**
|
* Join the list of strings into a single string with the specified
|
* separator.
|
*
|
* @param values
|
* @param separator
|
* @return joined list
|
*/
|
static String join(Collection<String> values, String separator) {
|
StringBuilder sb = new StringBuilder();
|
for (String value : values) {
|
sb.append(value).append(separator);
|
}
|
if (sb.length() > 0) {
|
// truncate trailing separator
|
sb.setLength(sb.length() - separator.length());
|
}
|
return sb.toString().trim();
|
}
|
|
|
/**
|
* Produce a deep copy of the given object. Serializes the entire object to
|
* a byte array in memory. Recommended for relatively small objects.
|
*/
|
@SuppressWarnings("unchecked")
|
static <T> T copy(T original) {
|
T o = null;
|
try {
|
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
|
ObjectOutputStream oos = new ObjectOutputStream(byteOut);
|
oos.writeObject(original);
|
ByteArrayInputStream byteIn = new ByteArrayInputStream(byteOut.toByteArray());
|
ObjectInputStream ois = new ObjectInputStream(byteIn);
|
try {
|
o = (T) ois.readObject();
|
} catch (ClassNotFoundException cex) {
|
// actually can not happen in this instance
|
}
|
} catch (IOException iox) {
|
// doesn't seem likely to happen as these streams are in memory
|
throw new RuntimeException(iox);
|
}
|
return o;
|
}
|
|
public static class Patchset implements Serializable, Comparable<Patchset> {
|
|
private static final long serialVersionUID = 1L;
|
|
public int number;
|
public int rev;
|
public String tip;
|
public String parent;
|
public String base;
|
public int insertions;
|
public int deletions;
|
public int commits;
|
public int added;
|
public PatchsetType type;
|
|
public boolean isFF() {
|
return PatchsetType.FastForward == type;
|
}
|
|
@Override
|
public int hashCode() {
|
return toString().hashCode();
|
}
|
|
@Override
|
public boolean equals(Object o) {
|
if (o instanceof Patchset) {
|
return hashCode() == o.hashCode();
|
}
|
return false;
|
}
|
|
@Override
|
public int compareTo(Patchset p) {
|
if (number > p.number) {
|
return -1;
|
} else if (p.number > number) {
|
return 1;
|
} else {
|
// same patchset, different revision
|
if (rev > p.rev) {
|
return -1;
|
} else if (p.rev > rev) {
|
return 1;
|
} else {
|
// same patchset & revision
|
return 0;
|
}
|
}
|
}
|
|
@Override
|
public String toString() {
|
return "patchset " + number + " revision " + rev;
|
}
|
}
|
|
public static class Comment implements Serializable {
|
|
private static final long serialVersionUID = 1L;
|
|
public String text;
|
|
public String id;
|
|
public Boolean deleted;
|
|
public CommentSource src;
|
|
public String replyTo;
|
|
Comment(String text) {
|
this.text = text;
|
}
|
|
public boolean isDeleted() {
|
return deleted != null && deleted;
|
}
|
|
@Override
|
public String toString() {
|
return text;
|
}
|
}
|
|
public static class Attachment implements Serializable {
|
|
private static final long serialVersionUID = 1L;
|
|
public final String name;
|
public long size;
|
public byte[] content;
|
public Boolean deleted;
|
|
public Attachment(String name) {
|
this.name = name;
|
}
|
|
public boolean isDeleted() {
|
return deleted != null && deleted;
|
}
|
|
@Override
|
public int hashCode() {
|
return name.hashCode();
|
}
|
|
@Override
|
public boolean equals(Object o) {
|
if (o instanceof Attachment) {
|
return name.equalsIgnoreCase(((Attachment) o).name);
|
}
|
return false;
|
}
|
|
@Override
|
public String toString() {
|
return name;
|
}
|
}
|
|
public static class Review implements Serializable {
|
|
private static final long serialVersionUID = 1L;
|
|
public final int patchset;
|
|
public final int rev;
|
|
public Score score;
|
|
public Review(int patchset, int revision) {
|
this.patchset = patchset;
|
this.rev = revision;
|
}
|
|
public boolean isReviewOf(Patchset p) {
|
return patchset == p.number && rev == p.rev;
|
}
|
|
@Override
|
public String toString() {
|
return "review of patchset " + patchset + " rev " + rev + ":" + score;
|
}
|
}
|
|
public static enum Score {
|
approved(2), looks_good(1), not_reviewed(0), needs_improvement(-1), vetoed(
|
-2);
|
|
final int value;
|
|
Score(int value) {
|
this.value = value;
|
}
|
|
public int getValue() {
|
return value;
|
}
|
|
@Override
|
public String toString() {
|
return name().toLowerCase().replace('_', ' ');
|
}
|
|
public static Score fromScore(int score) {
|
for (Score s : values()) {
|
if (s.getValue() == score) {
|
return s;
|
}
|
}
|
throw new NoSuchElementException(String.valueOf(score));
|
}
|
}
|
|
public static enum Field {
|
title, body, responsible, type, status, milestone, mergeSha, mergeTo,
|
topic, labels, watchers, reviewers, voters, mentions;
|
}
|
|
public static enum Type {
|
Enhancement, Task, Bug, Proposal, Question;
|
|
public static Type defaultType = Task;
|
|
public static Type [] choices() {
|
return new Type [] { Enhancement, Task, Bug, Question };
|
}
|
|
@Override
|
public String toString() {
|
return name().toLowerCase().replace('_', ' ');
|
}
|
|
public static Type fromObject(Object o, Type defaultType) {
|
if (o instanceof Type) {
|
// cast and return
|
return (Type) o;
|
} else if (o instanceof String) {
|
// find by name
|
for (Type type : values()) {
|
String str = o.toString();
|
if (type.name().equalsIgnoreCase(str)
|
|| type.toString().equalsIgnoreCase(str)) {
|
return type;
|
}
|
}
|
} else if (o instanceof Number) {
|
// by ordinal
|
int id = ((Number) o).intValue();
|
if (id >= 0 && id < values().length) {
|
return values()[id];
|
}
|
}
|
|
return defaultType;
|
}
|
}
|
|
public static enum Status {
|
New, Open, Closed, Resolved, Fixed, Merged, Wontfix, Declined, Duplicate, Invalid, Abandoned, On_Hold;
|
|
public static Status [] requestWorkflow = { Open, Resolved, Declined, Duplicate, Invalid, Abandoned, On_Hold };
|
|
public static Status [] bugWorkflow = { Open, Fixed, Wontfix, Duplicate, Invalid, Abandoned, On_Hold };
|
|
public static Status [] proposalWorkflow = { Open, Resolved, Declined, Abandoned, On_Hold };
|
|
public static Status [] milestoneWorkflow = { Open, Closed, Abandoned, On_Hold };
|
|
@Override
|
public String toString() {
|
return name().toLowerCase().replace('_', ' ');
|
}
|
|
public static Status fromObject(Object o, Status defaultStatus) {
|
if (o instanceof Status) {
|
// cast and return
|
return (Status) o;
|
} else if (o instanceof String) {
|
// find by name
|
String name = o.toString();
|
for (Status state : values()) {
|
if (state.name().equalsIgnoreCase(name)
|
|| state.toString().equalsIgnoreCase(name)) {
|
return state;
|
}
|
}
|
} else if (o instanceof Number) {
|
// by ordinal
|
int id = ((Number) o).intValue();
|
if (id >= 0 && id < values().length) {
|
return values()[id];
|
}
|
}
|
|
return defaultStatus;
|
}
|
|
public boolean isClosed() {
|
return ordinal() > Open.ordinal();
|
}
|
}
|
|
public static enum CommentSource {
|
Comment, Email
|
}
|
|
public static enum PatchsetType {
|
Proposal, FastForward, Rebase, Squash, Rebase_Squash, Amend;
|
|
public boolean isRewrite() {
|
return (this != FastForward) && (this != Proposal);
|
}
|
|
@Override
|
public String toString() {
|
return name().toLowerCase().replace('_', '+');
|
}
|
|
public static PatchsetType fromObject(Object o) {
|
if (o instanceof PatchsetType) {
|
// cast and return
|
return (PatchsetType) o;
|
} else if (o instanceof String) {
|
// find by name
|
String name = o.toString();
|
for (PatchsetType type : values()) {
|
if (type.name().equalsIgnoreCase(name)
|
|| type.toString().equalsIgnoreCase(name)) {
|
return type;
|
}
|
}
|
} else if (o instanceof Number) {
|
// by ordinal
|
int id = ((Number) o).intValue();
|
if (id >= 0 && id < values().length) {
|
return values()[id];
|
}
|
}
|
|
return null;
|
}
|
}
|
}
|