From c2188a840bc4153ae92112b04b2e06a90d3944aa Mon Sep 17 00:00:00 2001 From: Paul Martin <paul@paulsputer.com> Date: Wed, 27 Apr 2016 18:58:06 -0400 Subject: [PATCH] Ticket Reference handling #1048 --- src/main/java/com/gitblit/utils/JGitUtils.java | 329 +++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 files changed, 319 insertions(+), 10 deletions(-) diff --git a/src/main/java/com/gitblit/utils/JGitUtils.java b/src/main/java/com/gitblit/utils/JGitUtils.java index 7a00813..a02fc3f 100644 --- a/src/main/java/com/gitblit/utils/JGitUtils.java +++ b/src/main/java/com/gitblit/utils/JGitUtils.java @@ -21,12 +21,14 @@ 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.Iterator; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.Map.Entry; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,16 +38,24 @@ import org.eclipse.jgit.api.FetchCommand; import org.eclipse.jgit.api.Git; import org.eclipse.jgit.api.TagCommand; +import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException; import org.eclipse.jgit.api.errors.GitAPIException; +import org.eclipse.jgit.api.errors.JGitInternalException; import org.eclipse.jgit.diff.DiffEntry; import org.eclipse.jgit.diff.DiffEntry.ChangeType; import org.eclipse.jgit.diff.DiffFormatter; import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.dircache.DirCacheEntry; +import org.eclipse.jgit.errors.AmbiguousObjectException; import org.eclipse.jgit.errors.ConfigInvalidException; import org.eclipse.jgit.errors.IncorrectObjectTypeException; import org.eclipse.jgit.errors.LargeObjectException; import org.eclipse.jgit.errors.MissingObjectException; +import org.eclipse.jgit.errors.RevisionSyntaxException; import org.eclipse.jgit.errors.StopWalkException; +import org.eclipse.jgit.internal.JGitText; +import org.eclipse.jgit.lib.AnyObjectId; import org.eclipse.jgit.lib.BlobBasedConfig; import org.eclipse.jgit.lib.CommitBuilder; import org.eclipse.jgit.lib.Constants; @@ -63,6 +73,7 @@ import org.eclipse.jgit.lib.TreeFormatter; import org.eclipse.jgit.merge.MergeStrategy; import org.eclipse.jgit.merge.RecursiveMerger; +import org.eclipse.jgit.merge.ThreeWayMerger; import org.eclipse.jgit.revwalk.RevBlob; import org.eclipse.jgit.revwalk.RevCommit; import org.eclipse.jgit.revwalk.RevObject; @@ -75,6 +86,7 @@ import org.eclipse.jgit.transport.CredentialsProvider; import org.eclipse.jgit.transport.FetchResult; import org.eclipse.jgit.transport.RefSpec; +import org.eclipse.jgit.treewalk.CanonicalTreeParser; import org.eclipse.jgit.treewalk.TreeWalk; import org.eclipse.jgit.treewalk.filter.AndTreeFilter; import org.eclipse.jgit.treewalk.filter.OrTreeFilter; @@ -83,19 +95,22 @@ import org.eclipse.jgit.treewalk.filter.PathSuffixFilter; import org.eclipse.jgit.treewalk.filter.TreeFilter; import org.eclipse.jgit.util.FS; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import com.gitblit.GitBlit; import com.gitblit.GitBlitException; -import com.gitblit.manager.GitblitManager; +import com.gitblit.IStoredSettings; +import com.gitblit.Keys; +import com.gitblit.git.PatchsetCommand; import com.gitblit.models.FilestoreModel; import com.gitblit.models.GitNote; import com.gitblit.models.PathModel; import com.gitblit.models.PathModel.PathChangeModel; +import com.gitblit.models.TicketModel.TicketAction; +import com.gitblit.models.TicketModel.TicketLink; import com.gitblit.models.RefModel; import com.gitblit.models.SubmoduleModel; -import com.gitblit.servlet.FilestoreServlet; import com.google.common.base.Strings; /** @@ -1737,13 +1752,9 @@ * @return true if successful */ public static boolean deleteBranchRef(Repository repository, String branch) { - String branchName = branch; - if (!branchName.startsWith(Constants.R_HEADS)) { - branchName = Constants.R_HEADS + branch; - } try { - RefUpdate refUpdate = repository.updateRef(branchName, false); + RefUpdate refUpdate = repository.updateRef(branch, false); refUpdate.setForceUpdate(true); RefUpdate.Result result = refUpdate.delete(); switch (result) { @@ -1754,10 +1765,10 @@ return true; default: LOGGER.error(MessageFormat.format("{0} failed to delete to {1} returned result {2}", - repository.getDirectory().getAbsolutePath(), branchName, result)); + repository.getDirectory().getAbsolutePath(), branch, result)); } } catch (Throwable t) { - error(t, repository, "{0} failed to delete {1}", branchName); + error(t, repository, "{0} failed to delete {1}", branch); } return false; } @@ -2597,4 +2608,302 @@ + "objects/" + oid; } + + /** + * Returns all tree entries that do not match the ignore paths. + * + * @param db + * @param ignorePaths + * @param dcBuilder + * @throws IOException + */ + public static List<DirCacheEntry> getTreeEntries(Repository db, String branch, Collection<String> ignorePaths) throws IOException { + List<DirCacheEntry> list = new ArrayList<DirCacheEntry>(); + TreeWalk tw = null; + try { + ObjectId treeId = db.resolve(branch + "^{tree}"); + if (treeId == null) { + // branch does not exist yet + return list; + } + tw = new TreeWalk(db); + int hIdx = tw.addTree(treeId); + tw.setRecursive(true); + + while (tw.next()) { + String path = tw.getPathString(); + CanonicalTreeParser hTree = null; + if (hIdx != -1) { + hTree = tw.getTree(hIdx, CanonicalTreeParser.class); + } + if (!ignorePaths.contains(path)) { + // add all other tree entries + if (hTree != null) { + final DirCacheEntry entry = new DirCacheEntry(path); + entry.setObjectId(hTree.getEntryObjectId()); + entry.setFileMode(hTree.getEntryFileMode()); + list.add(entry); + } + } + } + } finally { + if (tw != null) { + tw.close(); + } + } + return list; + } + + public static boolean commitIndex(Repository db, String branch, DirCache index, + ObjectId parentId, boolean forceCommit, + String author, String authorEmail, String message) throws IOException, ConcurrentRefUpdateException { + boolean success = false; + + ObjectId headId = db.resolve(branch + "^{commit}"); + ObjectId baseId = parentId; + if (baseId == null || headId == null) { return false; } + + ObjectInserter odi = db.newObjectInserter(); + try { + // Create the in-memory index of the new/updated ticket + ObjectId indexTreeId = index.writeTree(odi); + + // Create a commit object + PersonIdent ident = new PersonIdent(author, authorEmail); + + if (forceCommit == false) { + ThreeWayMerger merger = MergeStrategy.RECURSIVE.newMerger(db, true); + merger.setObjectInserter(odi); + merger.setBase(baseId); + boolean mergeSuccess = merger.merge(indexTreeId, headId); + + if (mergeSuccess) { + indexTreeId = merger.getResultTreeId(); + } else { + //Manual merge required + return false; + } + } + + CommitBuilder commit = new CommitBuilder(); + commit.setAuthor(ident); + commit.setCommitter(ident); + commit.setEncoding(com.gitblit.Constants.ENCODING); + commit.setMessage(message); + commit.setParentId(headId); + commit.setTreeId(indexTreeId); + + // Insert the commit into the repository + ObjectId commitId = odi.insert(commit); + odi.flush(); + + RevWalk revWalk = new RevWalk(db); + try { + RevCommit revCommit = revWalk.parseCommit(commitId); + RefUpdate ru = db.updateRef(branch); + ru.setForceUpdate(forceCommit); + ru.setNewObjectId(commitId); + ru.setExpectedOldObjectId(headId); + ru.setRefLogMessage("commit: " + revCommit.getShortMessage(), false); + Result rc = ru.update(); + + switch (rc) { + case NEW: + case FORCED: + case FAST_FORWARD: + success = true; + break; + case REJECTED: + case LOCK_FAILURE: + throw new ConcurrentRefUpdateException(JGitText.get().couldNotLockHEAD, + ru.getRef(), rc); + default: + throw new JGitInternalException(MessageFormat.format( + JGitText.get().updatingRefFailed, branch, commitId.toString(), + rc)); + } + } finally { + revWalk.close(); + } + } finally { + odi.close(); + } + return success; + } + + /** + * Returns true if the commit identified by commitId is at the tip of it's branch. + * + * @param repository + * @param commitId + * @return true if the given commit is the tip + */ + public static boolean isTip(Repository repository, String commitId) { + try { + RefModel tip = getBranch(repository, commitId); + return (tip != null); + } catch (Exception e) { + LOGGER.error("Failed to determine isTip", e); + } + return false; + } + + /* + * Identify ticket by considering the branch the commit is on + * + * @param repository + * @param commit + * @return ticket number, or 0 if no ticket + */ + public static long getTicketNumberFromCommitBranch(Repository repository, RevCommit commit) { + // try lookup by change ref + Map<AnyObjectId, Set<Ref>> map = repository.getAllRefsByPeeledObjectId(); + Set<Ref> refs = map.get(commit.getId()); + if (!ArrayUtils.isEmpty(refs)) { + for (Ref ref : refs) { + long number = PatchsetCommand.getTicketNumber(ref.getName()); + + if (number > 0) { + return number; + } + } + } + + return 0; + } + + + /** + * Try to identify all referenced tickets from the commit. + * + * @param commit + * @return a collection of TicketLinks + */ + @NotNull + public static List<TicketLink> identifyTicketsFromCommitMessage(Repository repository, IStoredSettings settings, + RevCommit commit) { + List<TicketLink> ticketLinks = new ArrayList<TicketLink>(); + List<Long> linkedTickets = new ArrayList<Long>(); + + // parse commit message looking for fixes/closes #n + final String xFixDefault = "(?:fixes|closes)[\\s-]+#?(\\d+)"; + String xFix = settings.getString(Keys.tickets.closeOnPushCommitMessageRegex, xFixDefault); + if (StringUtils.isEmpty(xFix)) { + xFix = xFixDefault; + } + try { + Pattern p = Pattern.compile(xFix, Pattern.CASE_INSENSITIVE); + Matcher m = p.matcher(commit.getFullMessage()); + while (m.find()) { + String val = m.group(1); + long number = Long.parseLong(val); + + if (number > 0) { + ticketLinks.add(new TicketLink(number, TicketAction.Close)); + linkedTickets.add(number); + } + } + } catch (Exception e) { + LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xFix, commit.getName()), e); + } + + // parse commit message looking for ref #n + final String xRefDefault = "(?:ref|task|issue|bug)?[\\s-]*#(\\d+)"; + String xRef = settings.getString(Keys.tickets.linkOnPushCommitMessageRegex, xRefDefault); + if (StringUtils.isEmpty(xRef)) { + xRef = xRefDefault; + } + try { + Pattern p = Pattern.compile(xRef, Pattern.CASE_INSENSITIVE); + Matcher m = p.matcher(commit.getFullMessage()); + while (m.find()) { + String val = m.group(1); + long number = Long.parseLong(val); + //Most generic case so don't included tickets more precisely linked + if ((number > 0) && (!linkedTickets.contains(number))) { + ticketLinks.add( new TicketLink(number, TicketAction.Commit, commit.getName())); + linkedTickets.add(number); + } + } + } catch (Exception e) { + LOGGER.error(String.format("Failed to parse \"%s\" in commit %s", xRef, commit.getName()), e); + } + + return ticketLinks; + } + + /** + * Try to identify all referenced tickets between two commits + * + * @param commit + * @param parseMessage + * @param currentTicketId, or 0 if not on a ticket branch + * @return a collection of TicketLink, or null if commit is already linked + */ + public static List<TicketLink> identifyTicketsBetweenCommits(Repository repository, IStoredSettings settings, + String baseSha, String tipSha) { + List<TicketLink> links = new ArrayList<TicketLink>(); + if (repository == null) { return links; } + + RevWalk walk = new RevWalk(repository); + walk.sort(RevSort.TOPO); + walk.sort(RevSort.REVERSE, true); + try { + RevCommit tip = walk.parseCommit(repository.resolve(tipSha)); + RevCommit base = walk.parseCommit(repository.resolve(baseSha)); + walk.markStart(tip); + walk.markUninteresting(base); + for (;;) { + RevCommit commit = walk.next(); + if (commit == null) { + break; + } + links.addAll(JGitUtils.identifyTicketsFromCommitMessage(repository, settings, commit)); + } + } catch (IOException e) { + LOGGER.error("failed to identify tickets between commits.", e); + } finally { + walk.dispose(); + } + + return links; + } + + public static int countCommits(Repository repository, RevWalk walk, ObjectId baseId, ObjectId tipId) { + int count = 0; + walk.reset(); + walk.sort(RevSort.TOPO); + walk.sort(RevSort.REVERSE, true); + try { + RevCommit tip = walk.parseCommit(tipId); + RevCommit base = walk.parseCommit(baseId); + walk.markStart(tip); + walk.markUninteresting(base); + for (;;) { + RevCommit c = walk.next(); + if (c == null) { + break; + } + count++; + } + } catch (IOException e) { + // Should never happen, the core receive process would have + // identified the missing object earlier before we got control. + LOGGER.error("failed to get commit count", e); + return 0; + } finally { + walk.close(); + } + return count; + } + + public static int countCommits(Repository repository, RevWalk walk, String baseId, String tipId) { + int count = 0; + try { + count = countCommits(repository, walk, repository.resolve(baseId),repository.resolve(tipId)); + } catch (IOException e) { + LOGGER.error("failed to get commit count", e); + } + return count; + } } -- Gitblit v1.9.1