diff --git a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF index a0a5ed038..76b135508 100644 --- a/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF +++ b/org.eclipse.jgit.pgm/META-INF/MANIFEST.MF @@ -9,6 +9,7 @@ Bundle-RequiredExecutionEnvironment: J2SE-1.5 Import-Package: org.eclipse.jgit.api;version="[1.1.0,1.2.0)", org.eclipse.jgit.api.errors;version="[1.1.0,1.2.0)", org.eclipse.jgit.awtui;version="[1.1.0,1.2.0)", + org.eclipse.jgit.blame;version="[1.1.0,1.2.0)", org.eclipse.jgit.diff;version="[1.1.0,1.2.0)", org.eclipse.jgit.dircache;version="[1.1.0,1.2.0)", org.eclipse.jgit.errors;version="[1.1.0,1.2.0)", diff --git a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin index 1586528c6..6562423f8 100644 --- a/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin +++ b/org.eclipse.jgit.pgm/META-INF/services/org.eclipse.jgit.pgm.TextBuiltin @@ -1,5 +1,6 @@ org.eclipse.jgit.pgm.Add org.eclipse.jgit.pgm.AmazonS3Client +org.eclipse.jgit.pgm.Blame org.eclipse.jgit.pgm.Branch org.eclipse.jgit.pgm.Checkout org.eclipse.jgit.pgm.Clone diff --git a/org.eclipse.jgit.pgm/jgit.sh b/org.eclipse.jgit.pgm/jgit.sh index 9ff59d612..f18e85fe4 100644 --- a/org.eclipse.jgit.pgm/jgit.sh +++ b/org.eclipse.jgit.pgm/jgit.sh @@ -52,6 +52,7 @@ done use_pager= case "$cmd" in +blame) use_pager=1 ;; diff) use_pager=1 ;; log) use_pager=1 ;; esac diff --git a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties index 98fbd7fbc..1c95fd5f9 100644 --- a/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties +++ b/org.eclipse.jgit.pgm/resources/org/eclipse/jgit/pgm/CLIText.properties @@ -51,6 +51,7 @@ failedToLockTag=Failed to lock tag {0}: {1} fatalError=fatal: {0} fatalErrorTagExists=fatal: tag '{0}' exists fatalThisProgramWillDestroyTheRepository=fatal: This program will destroy the repository\nfatal:\nfatal:\nfatal: {0}\nfatal:\nfatal: To continue, add {1} to the command line\nfatal: +fileIsRequired=argument file is required forcedUpdate=forced update fromURI=From {0} initializedEmptyGitRepositoryIn=Initialized empty Git repository in {0} @@ -65,6 +66,8 @@ metaVar_KEY=KEY metaVar_arg=ARG metaVar_author=AUTHOR metaVar_base=base +metaVar_blameL=START,END +metaVar_blameReverse=START..END metaVar_bucket=BUCKET metaVar_command=command metaVar_commandDetail=DETAIL @@ -93,6 +96,7 @@ metaVar_ref=REF metaVar_refs=REFS metaVar_refspec=refspec metaVar_remoteName=name +metaVar_revision=REVISION metaVar_seconds=SECONDS metaVar_service=SERVICE metaVar_treeish=tree-ish @@ -132,6 +136,7 @@ timeInMilliSeconds={0} ms tooManyRefsGiven=Too many refs given unknownMergeStratey=unknown merge strategy {0} specified unsupportedOperation=Unsupported operation: {0} +usage_Blame=Show what revision and author last modified each line usage_CommandLineClientForamazonsS3Service=Command line client for Amazon's S3 service usage_CommitAuthor=Override the author name used in the commit. You can use the standard A U Thor format. usage_CommitMessage=Use the given as the commit message @@ -152,11 +157,22 @@ usage_ServerSideBackendForJgitPush=Server side backend for 'jgit push' usage_ShowDiffs=Show diffs usage_StopTrackingAFile=Stop tracking a file usage_UpdateRemoteRepositoryFromLocalRefs=Update remote repository from local refs +usage_abbrevCommits=abbreviate commits to N + 1 digits usage_abortConnectionIfNoActivity=abort connection if no activity usage_actOnRemoteTrackingBranches=act on remote-tracking branches usage_addFileContentsToTheIndex=Add file contents to the index usage_alterTheDetailShown=alter the detail shown usage_approveDestructionOfRepository=approve destruction of repository +usage_blameLongRevision=show long revision +usage_blameRange=annotate only the given range +usage_blameRawTimestamp=show raw timestamp +usage_blameReverse=show origin of deletions instead of insertions +usage_blameShowBlankBoundary=show blank SHA-1 for boundary commits +usage_blameShowEmail=show author email instead of name +usage_blameShowRoot=do not treat root commits as boundaries +usage_blameShowSourceLine=show source line number +usage_blameShowSourcePath=show source filename +usage_blameSuppressAuthor=do not show author name and timestamp usage_beMoreVerbose=be more verbose usage_beVerbose=be verbose usage_cached=compare against index @@ -188,6 +204,7 @@ usage_forceCreateBranchEvenExists=force create branch even exists usage_forceReplacingAnExistingTag=force replacing an existing tag usage_hostnameOrIpToListenOn=hostname (or ip) to listen on usage_indexFileFormatToCreate=index file format to create +usage_ignoreWhitespace=ignore all whitespace usage_inputOutputFile=Input/output file usage_listBothRemoteTrackingAndLocalBranches=list both remote-tracking and local branches usage_listCreateOrDeleteBranches=List, create, or delete branches diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java new file mode 100644 index 000000000..162f433ff --- /dev/null +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/Blame.java @@ -0,0 +1,350 @@ +/* + * Copyright (C) 2011, Google Inc. + * Copyright (C) 2009, Christian Halstrick + * Copyright (C) 2009, Johannes E. Schindelin + * Copyright (C) 2009, Johannes Schindelin + * and other copyright owners as documented in the project's IP log. + * + * This program and the accompanying materials are made available + * under the terms of the Eclipse Distribution License v1.0 which + * accompanies this distribution, is reproduced below, and is + * available at http://www.eclipse.org/org/documents/edl-v10.php + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or + * without modification, are permitted provided that the following + * conditions are met: + * + * - Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * - Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following + * disclaimer in the documentation and/or other materials provided + * with the distribution. + * + * - Neither the name of the Eclipse Foundation, Inc. nor the + * names of its contributors may be used to endorse or promote + * products derived from this software without specific prior + * written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND + * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, + * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE + * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, + * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) + * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF + * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +package org.eclipse.jgit.pgm; + +import static org.eclipse.jgit.lib.Constants.OBJECT_ID_STRING_LENGTH; + +import java.io.File; +import java.io.IOException; +import java.text.MessageFormat; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.regex.Pattern; + +import org.eclipse.jgit.blame.BlameGenerator; +import org.eclipse.jgit.blame.BlameResult; +import org.eclipse.jgit.diff.RawText; +import org.eclipse.jgit.diff.RawTextComparator; +import org.eclipse.jgit.dircache.DirCache; +import org.eclipse.jgit.lib.Constants; +import org.eclipse.jgit.lib.ObjectReader; +import org.eclipse.jgit.lib.PersonIdent; +import org.eclipse.jgit.revwalk.RevCommit; +import org.eclipse.jgit.revwalk.RevFlag; +import org.kohsuke.args4j.Argument; +import org.kohsuke.args4j.Option; + +@Command(common = false, usage = "usage_Blame") +class Blame extends TextBuiltin { + private RawTextComparator comparator = RawTextComparator.DEFAULT; + + @Option(name = "-w", usage = "usage_ignoreWhitespace") + void ignoreAllSpace(@SuppressWarnings("unused") boolean on) { + comparator = RawTextComparator.WS_IGNORE_ALL; + } + + @Option(name = "--abbrev", metaVar = "metaVar_n", usage = "usage_abbrevCommits") + private int abbrev; + + @Option(name = "-l", usage = "usage_blameLongRevision") + private boolean showLongRevision; + + @Option(name = "-t", usage = "usage_blameRawTimestamp") + private boolean showRawTimestamp; + + @Option(name = "-b", usage = "usage_blameShowBlankBoundary") + private boolean showBlankBoundary; + + @Option(name = "-s", usage = "usage_blameSuppressAuthor") + private boolean noAuthor; + + @Option(name = "--show-email", aliases = { "-e" }, usage = "usage_blameShowEmail") + private boolean showAuthorEmail; + + @Option(name = "--show-name", aliases = { "-f" }, usage = "usage_blameShowSourcePath") + private boolean showSourcePath; + + @Option(name = "--show-number", aliases = { "-n" }, usage = "usage_blameShowSourceLine") + private boolean showSourceLine; + + @Option(name = "--root", usage = "usage_blameShowRoot") + private boolean root; + + @Option(name = "-L", metaVar = "metaVar_blameL", usage = "usage_blameRange") + private String rangeString; + + @Option(name = "--reverse", metaVar = "metaVar_blameReverse", usage = "usage_blameReverse") + private List reverseRange = new ArrayList(2); + + @Argument(index = 0, required = false, metaVar = "metaVar_revision") + private String revision; + + @Argument(index = 1, required = false, metaVar = "metaVar_file") + private String file; + + private ObjectReader reader; + + private final Map abbreviatedCommits = new HashMap(); + + private SimpleDateFormat dateFmt; + + private int begin; + + private int end; + + private BlameResult blame; + + @Override + protected void run() throws Exception { + if (file == null) { + if (revision == null) + throw die(CLIText.get().fileIsRequired); + file = revision; + revision = null; + } + + if (abbrev == 0) + abbrev = db.getConfig().getInt("core", "abbrev", 7); + if (!showBlankBoundary) + root = db.getConfig().getBoolean("blame", "blankboundary", false); + if (!root) + root = db.getConfig().getBoolean("blame", "showroot", false); + + if (showRawTimestamp) + dateFmt = new SimpleDateFormat("ZZZZ"); + else + dateFmt = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss ZZZZ"); + + BlameGenerator generator = new BlameGenerator(db, file); + reader = db.newObjectReader(); + try { + generator.setTextComparator(comparator); + + if (!reverseRange.isEmpty()) { + RevCommit rangeStart = null; + List rangeEnd = new ArrayList(2); + for (RevCommit c : reverseRange) { + if (c.has(RevFlag.UNINTERESTING)) + rangeStart = c; + else + rangeEnd.add(c); + } + generator.reverse(rangeStart, rangeEnd); + } else if (revision != null) { + generator.push(null, db.resolve(revision + "^{commit}")); + } else { + generator.push(null, db.resolve(Constants.HEAD)); + if (!db.isBare()) { + DirCache dc = db.readDirCache(); + int entry = dc.findEntry(file); + if (0 <= entry) + generator.push(null, dc.getEntry(entry).getObjectId()); + + File inTree = new File(db.getWorkTree(), file); + if (inTree.isFile()) + generator.push(null, new RawText(inTree)); + } + } + + blame = BlameResult.create(generator); + begin = 0; + end = blame.getResultContents().size(); + if (rangeString != null) + parseLineRangeOption(); + blame.computeRange(begin, end); + + int authorWidth = 8; + int dateWidth = 8; + int pathWidth = 1; + int maxSourceLine = 1; + for (int line = begin; line < end; line++) { + authorWidth = Math.max(authorWidth, author(line).length()); + dateWidth = Math.max(dateWidth, date(line).length()); + pathWidth = Math.max(pathWidth, path(line).length()); + maxSourceLine = Math.max(maxSourceLine, blame.getSourceLine(line)); + } + + String pathFmt = MessageFormat.format(" %{0}s", pathWidth); + String numFmt = MessageFormat.format(" %{0}d", + 1 + (int) Math.log10(maxSourceLine + 1)); + String lineFmt = MessageFormat.format(" %{0}d) ", + 1 + (int) Math.log10(end + 1)); + String authorFmt = MessageFormat.format(" (%-{0}s %{1}s", + authorWidth, dateWidth); + + for (int line = begin; line < end; line++) { + out.print(abbreviate(blame.getSourceCommit(line))); + if (showSourcePath) + out.format(pathFmt, path(line)); + if (showSourceLine) + out.format(numFmt, blame.getSourceLine(line) + 1); + if (!noAuthor) + out.format(authorFmt, author(line), date(line)); + out.format(lineFmt, line + 1); + out.flush(); + blame.getResultContents().writeLine(System.out, line); + out.print('\n'); + } + } finally { + generator.release(); + reader.release(); + } + } + + private void parseLineRangeOption() { + String beginStr, endStr; + if (rangeString.startsWith("/")) { + int c = rangeString.indexOf("/,", 1); + if (c < 0) { + beginStr = rangeString; + endStr = String.valueOf(end); + } else { + beginStr = rangeString.substring(0, c); + endStr = rangeString.substring(c + 2); + } + + } else { + int c = rangeString.indexOf(','); + if (c < 0) { + beginStr = rangeString; + endStr = String.valueOf(end); + } else if (c == 0) { + beginStr = "0"; + endStr = rangeString.substring(1); + } else { + beginStr = rangeString.substring(0, c); + endStr = rangeString.substring(c + 1); + } + } + + if (beginStr.equals("")) + begin = 0; + else if (beginStr.startsWith("/")) + begin = findLine(0, beginStr); + else + begin = Math.max(0, Integer.parseInt(beginStr) - 1); + + if (endStr.equals("")) + end = blame.getResultContents().size(); + else if (endStr.startsWith("/")) + end = findLine(begin, endStr); + else if (endStr.startsWith("-")) + end = begin + Integer.parseInt(endStr); + else if (endStr.startsWith("+")) + end = begin + Integer.parseInt(endStr.substring(1)); + else + end = Math.max(0, Integer.parseInt(endStr) - 1); + } + + private int findLine(int b, String regex) { + String re = regex.substring(1, regex.length() - 1); + if (!re.startsWith("^")) + re = ".*" + re; + if (!re.endsWith("$")) + re = re + ".*"; + Pattern p = Pattern.compile(re); + RawText text = blame.getResultContents(); + for (int line = b; line < text.size(); line++) { + if (p.matcher(text.getString(line)).matches()) + return line; + } + return b; + } + + private String path(int line) { + String p = blame.getSourcePath(line); + return p != null ? p : ""; + } + + private String author(int line) { + PersonIdent author = blame.getSourceAuthor(line); + if (author == null) + return ""; + String name = showAuthorEmail ? author.getEmailAddress() : author + .getName(); + return name != null ? name : ""; + } + + private String date(int line) { + if (blame.getSourceCommit(line) == null) + return ""; + + PersonIdent author = blame.getSourceAuthor(line); + if (author == null) + return ""; + + dateFmt.setTimeZone(author.getTimeZone()); + if (!showRawTimestamp) + return dateFmt.format(author.getWhen()); + return String.format("%d %s", author.getWhen().getTime() / 1000L, + dateFmt.format(author.getWhen())); + } + + private String abbreviate(RevCommit commit) throws IOException { + String r = abbreviatedCommits.get(commit); + if (r != null) + return r; + + if (showBlankBoundary && commit.getParentCount() == 0) + commit = null; + + if (commit == null) { + int len = showLongRevision ? OBJECT_ID_STRING_LENGTH : (abbrev + 1); + StringBuilder b = new StringBuilder(len); + for (int i = 0; i < len; i++) + b.append(' '); + r = b.toString(); + + } else if (!root && commit.getParentCount() == 0) { + if (showLongRevision) + r = "^" + commit.name().substring(0, OBJECT_ID_STRING_LENGTH - 1); + else + r = "^" + reader.abbreviate(commit, abbrev).name(); + } else { + if (showLongRevision) + r = commit.name(); + else + r = reader.abbreviate(commit, abbrev + 1).name(); + } + + abbreviatedCommits.put(commit, r); + return r; + } +} diff --git a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java index d82ff499f..e1c26adf4 100644 --- a/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java +++ b/org.eclipse.jgit.pgm/src/org/eclipse/jgit/pgm/CLIText.java @@ -104,6 +104,7 @@ public static CLIText get() { /***/ public String fatalError; /***/ public String fatalErrorTagExists; /***/ public String fatalThisProgramWillDestroyTheRepository; + /***/ public String fileIsRequired; /***/ public String forcedUpdate; /***/ public String fromURI; /***/ public String initializedEmptyGitRepositoryIn;