Skip to content

Commit c585040

Browse files
authored
Merge pull request #1055 from gitblit/1048-TicketReferences
Ticket Reference handling #1048
2 parents bbb65e0 + 7f186f1 commit c585040

File tree

14 files changed

+1926
-201
lines changed

14 files changed

+1926
-201
lines changed

src/main/distrib/data/defaults.properties

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -574,6 +574,13 @@ tickets.requireApproval = false
574574
# SINCE 1.5.0
575575
tickets.closeOnPushCommitMessageRegex = (?:fixes|closes)[\\s-]+#?(\\d+)
576576

577+
# The case-insensitive regular expression used to identify and link tickets on
578+
# push to the commits based on commit message. In the case of a patchset
579+
# self references are ignored
580+
#
581+
# SINCE 1.8.0
582+
tickets.linkOnPushCommitMessageRegex = (?:ref|task|issue|bug)?[\\s-]*#(\\d+)
583+
577584
# Specify the location of the Lucene Ticket index
578585
#
579586
# SINCE 1.4.0

src/main/java/com/gitblit/git/GitblitReceivePack.java

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,28 @@
2222
import java.io.File;
2323
import java.io.IOException;
2424
import java.text.MessageFormat;
25+
import java.util.ArrayList;
2526
import java.util.Collection;
27+
import java.util.LinkedHashMap;
2628
import java.util.LinkedHashSet;
2729
import java.util.List;
30+
import java.util.Map;
2831
import java.util.Set;
32+
import java.util.SortedMap;
33+
import java.util.TreeMap;
2934
import java.util.concurrent.TimeUnit;
3035

36+
import org.eclipse.jgit.lib.AnyObjectId;
3137
import org.eclipse.jgit.lib.BatchRefUpdate;
3238
import org.eclipse.jgit.lib.NullProgressMonitor;
39+
import org.eclipse.jgit.lib.ObjectId;
3340
import org.eclipse.jgit.lib.PersonIdent;
3441
import org.eclipse.jgit.lib.ProgressMonitor;
42+
import org.eclipse.jgit.lib.Ref;
43+
import org.eclipse.jgit.lib.RefUpdate;
3544
import org.eclipse.jgit.lib.Repository;
3645
import org.eclipse.jgit.revwalk.RevCommit;
46+
import org.eclipse.jgit.revwalk.RevWalk;
3747
import org.eclipse.jgit.transport.PostReceiveHook;
3848
import org.eclipse.jgit.transport.PreReceiveHook;
3949
import org.eclipse.jgit.transport.ReceiveCommand;
@@ -50,14 +60,24 @@
5060
import com.gitblit.extensions.ReceiveHook;
5161
import com.gitblit.manager.IGitblit;
5262
import com.gitblit.models.RepositoryModel;
63+
import com.gitblit.models.TicketModel;
5364
import com.gitblit.models.UserModel;
65+
import com.gitblit.models.TicketModel.Change;
66+
import com.gitblit.models.TicketModel.Field;
67+
import com.gitblit.models.TicketModel.Patchset;
68+
import com.gitblit.models.TicketModel.Status;
69+
import com.gitblit.models.TicketModel.TicketAction;
70+
import com.gitblit.models.TicketModel.TicketLink;
5471
import com.gitblit.tickets.BranchTicketService;
72+
import com.gitblit.tickets.ITicketService;
73+
import com.gitblit.tickets.TicketNotifier;
5574
import com.gitblit.utils.ArrayUtils;
5675
import com.gitblit.utils.ClientLogger;
5776
import com.gitblit.utils.CommitCache;
5877
import com.gitblit.utils.JGitUtils;
5978
import com.gitblit.utils.RefLogUtils;
6079
import com.gitblit.utils.StringUtils;
80+
import com.google.common.collect.Lists;
6181

6282

6383
/**
@@ -92,6 +112,11 @@ public class GitblitReceivePack extends ReceivePack implements PreReceiveHook, P
92112
protected final IStoredSettings settings;
93113

94114
protected final IGitblit gitblit;
115+
116+
protected final ITicketService ticketService;
117+
118+
protected final TicketNotifier ticketNotifier;
119+
95120

96121
public GitblitReceivePack(
97122
IGitblit gitblit,
@@ -114,6 +139,14 @@ public GitblitReceivePack(
114139
} catch (IOException e) {
115140
}
116141

142+
if (gitblit.getTicketService().isAcceptingTicketUpdates(repository)) {
143+
this.ticketService = gitblit.getTicketService();
144+
this.ticketNotifier = this.ticketService.createNotifier();
145+
} else {
146+
this.ticketService = null;
147+
this.ticketNotifier = null;
148+
}
149+
117150
// set advanced ref permissions
118151
setAllowCreates(user.canCreateRef(repository));
119152
setAllowDeletes(user.canDeleteRef(repository));
@@ -500,6 +533,104 @@ protected void executeCommands() {
500533
}
501534
}
502535
}
536+
537+
//
538+
// if there are ref update receive commands that were
539+
// successfully processed and there is an active ticket service for the repository
540+
// then process any referenced tickets
541+
//
542+
if (ticketService != null) {
543+
List<ReceiveCommand> allUpdates = ReceiveCommand.filter(batch.getCommands(), Result.OK);
544+
if (!allUpdates.isEmpty()) {
545+
int ticketsProcessed = 0;
546+
for (ReceiveCommand cmd : allUpdates) {
547+
switch (cmd.getType()) {
548+
case CREATE:
549+
case UPDATE:
550+
if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
551+
Collection<TicketModel> tickets = processReferencedTickets(cmd);
552+
ticketsProcessed += tickets.size();
553+
for (TicketModel ticket : tickets) {
554+
ticketNotifier.queueMailing(ticket);
555+
}
556+
}
557+
break;
558+
559+
case UPDATE_NONFASTFORWARD:
560+
if (cmd.getRefName().startsWith(Constants.R_HEADS)) {
561+
String base = JGitUtils.getMergeBase(getRepository(), cmd.getOldId(), cmd.getNewId());
562+
List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(), settings, base, cmd.getOldId().name());
563+
for (TicketLink link : deletedRefs) {
564+
link.isDelete = true;
565+
}
566+
Change deletion = new Change(user.username);
567+
deletion.pendingLinks = deletedRefs;
568+
ticketService.updateTicket(repository, 0, deletion);
569+
570+
Collection<TicketModel> tickets = processReferencedTickets(cmd);
571+
ticketsProcessed += tickets.size();
572+
for (TicketModel ticket : tickets) {
573+
ticketNotifier.queueMailing(ticket);
574+
}
575+
}
576+
break;
577+
case DELETE:
578+
//Identify if the branch has been merged
579+
SortedMap<Integer, String> bases = new TreeMap<Integer, String>();
580+
try {
581+
ObjectId dObj = cmd.getOldId();
582+
Collection<Ref> tips = getRepository().getRefDatabase().getRefs(Constants.R_HEADS).values();
583+
for (Ref ref : tips) {
584+
ObjectId iObj = ref.getObjectId();
585+
String mergeBase = JGitUtils.getMergeBase(getRepository(), dObj, iObj);
586+
if (mergeBase != null) {
587+
int d = JGitUtils.countCommits(getRepository(), getRevWalk(), mergeBase, dObj.name());
588+
bases.put(d, mergeBase);
589+
//All commits have been merged into some other branch
590+
if (d == 0) {
591+
break;
592+
}
593+
}
594+
}
595+
596+
if (bases.isEmpty()) {
597+
//TODO: Handle orphan branch case
598+
} else {
599+
if (bases.firstKey() > 0) {
600+
//Delete references from the remaining commits that haven't been merged
601+
String mergeBase = bases.get(bases.firstKey());
602+
List<TicketLink> deletedRefs = JGitUtils.identifyTicketsBetweenCommits(getRepository(),
603+
settings, mergeBase, dObj.name());
604+
605+
for (TicketLink link : deletedRefs) {
606+
link.isDelete = true;
607+
}
608+
Change deletion = new Change(user.username);
609+
deletion.pendingLinks = deletedRefs;
610+
ticketService.updateTicket(repository, 0, deletion);
611+
}
612+
}
613+
614+
} catch (IOException e) {
615+
LOGGER.error(null, e);
616+
}
617+
break;
618+
619+
default:
620+
break;
621+
}
622+
}
623+
624+
if (ticketsProcessed == 1) {
625+
sendInfo("1 ticket updated");
626+
} else if (ticketsProcessed > 1) {
627+
sendInfo("{0} tickets updated", ticketsProcessed);
628+
}
629+
}
630+
631+
// reset the ticket caches for the repository
632+
ticketService.resetCaches(repository);
633+
}
503634
}
504635

505636
protected void setGitblitUrl(String url) {
@@ -616,4 +747,116 @@ public RepositoryModel getRepositoryModel() {
616747
public UserModel getUserModel() {
617748
return user;
618749
}
750+
751+
/**
752+
* Automatically closes open tickets and adds references to tickets if made in the commit message.
753+
*
754+
* @param cmd
755+
*/
756+
private Collection<TicketModel> processReferencedTickets(ReceiveCommand cmd) {
757+
Map<Long, TicketModel> changedTickets = new LinkedHashMap<Long, TicketModel>();
758+
759+
final RevWalk rw = getRevWalk();
760+
try {
761+
rw.reset();
762+
rw.markStart(rw.parseCommit(cmd.getNewId()));
763+
if (!ObjectId.zeroId().equals(cmd.getOldId())) {
764+
rw.markUninteresting(rw.parseCommit(cmd.getOldId()));
765+
}
766+
767+
RevCommit c;
768+
while ((c = rw.next()) != null) {
769+
rw.parseBody(c);
770+
List<TicketLink> ticketLinks = JGitUtils.identifyTicketsFromCommitMessage(getRepository(), settings, c);
771+
if (ticketLinks == null) {
772+
continue;
773+
}
774+
775+
for (TicketLink link : ticketLinks) {
776+
777+
TicketModel ticket = ticketService.getTicket(repository, link.targetTicketId);
778+
if (ticket == null) {
779+
continue;
780+
}
781+
782+
Change change = null;
783+
String commitSha = c.getName();
784+
String branchName = Repository.shortenRefName(cmd.getRefName());
785+
786+
switch (link.action) {
787+
case Commit: {
788+
//A commit can reference a ticket in any branch even if the ticket is closed.
789+
//This allows developers to identify and communicate related issues
790+
change = new Change(user.username);
791+
change.referenceCommit(commitSha);
792+
} break;
793+
794+
case Close: {
795+
// As this isn't a patchset theres no merging taking place when closing a ticket
796+
if (ticket.isClosed()) {
797+
continue;
798+
}
799+
800+
change = new Change(user.username);
801+
change.setField(Field.status, Status.Fixed);
802+
803+
if (StringUtils.isEmpty(ticket.responsible)) {
804+
// unassigned tickets are assigned to the closer
805+
change.setField(Field.responsible, user.username);
806+
}
807+
}
808+
809+
default: {
810+
//No action
811+
} break;
812+
}
813+
814+
if (change != null) {
815+
ticket = ticketService.updateTicket(repository, ticket.number, change);
816+
}
817+
818+
if (ticket != null) {
819+
sendInfo("");
820+
sendHeader("#{0,number,0}: {1}", ticket.number, StringUtils.trimString(ticket.title, Constants.LEN_SHORTLOG));
821+
822+
switch (link.action) {
823+
case Commit: {
824+
sendInfo("referenced by push of {0} to {1}", commitSha, branchName);
825+
changedTickets.put(ticket.number, ticket);
826+
} break;
827+
828+
case Close: {
829+
sendInfo("closed by push of {0} to {1}", commitSha, branchName);
830+
changedTickets.put(ticket.number, ticket);
831+
} break;
832+
833+
default: { }
834+
}
835+
836+
sendInfo(ticketService.getTicketUrl(ticket));
837+
sendInfo("");
838+
} else {
839+
switch (link.action) {
840+
case Commit: {
841+
sendError("FAILED to reference ticket {0} by push of {1}", link.targetTicketId, commitSha);
842+
} break;
843+
844+
case Close: {
845+
sendError("FAILED to close ticket {0} by push of {1}", link.targetTicketId, commitSha);
846+
} break;
847+
848+
default: { }
849+
}
850+
}
851+
}
852+
}
853+
854+
} catch (IOException e) {
855+
LOGGER.error("Can't scan for changes to reference or close", e);
856+
} finally {
857+
rw.reset();
858+
}
859+
860+
return changedTickets.values();
861+
}
619862
}

0 commit comments

Comments
 (0)