log, diff: Add rename detection support
Implement rename detection in the command line diff and log commands. Also support --name-status, -p and -U flags, as these can be quite useful to view more detail. All of the Git patch file formatting code is now moved over to the DiffFormatter class. This permits us to reuse it in any context, including inside of IDEs. Change-Id: I687ccba34e18105a07e0a439d2181c323209d96c Signed-off-by: Shawn O. Pearce <spearce@spearce.org>
This commit is contained in:
parent
978535b090
commit
04a9d23b9a
|
@ -66,6 +66,7 @@ metaVar_directory=DIRECTORY
|
|||
metaVar_file=FILE
|
||||
metaVar_gitDir=GIT_DIR
|
||||
metaVar_hostName=HOSTNAME
|
||||
metaVar_linesOfContext=lines
|
||||
metaVar_message=message
|
||||
metaVar_name=name
|
||||
metaVar_object=object
|
||||
|
@ -139,6 +140,7 @@ usage_cloneRepositoryIntoNewDir=Clone a repository into a new directory
|
|||
usage_configureTheServiceInDaemonServicename=configure the service in daemon.servicename
|
||||
usage_deleteBranchEvenIfNotMerged=delete branch (even if not merged)
|
||||
usage_deleteFullyMergedBranch=delete fully merged branch
|
||||
usage_detectRenames=detect renamed files
|
||||
usage_directoriesToExport=directories to export
|
||||
usage_disableTheServiceInAllRepositories=disable the service in all repositories
|
||||
usage_displayAListOfAllRegisteredJgitCommands=Display a list of all registered jgit commands
|
||||
|
@ -160,6 +162,7 @@ usage_listBothRemoteTrackingAndLocalBranches=list both remote-tracking and local
|
|||
usage_listCreateOrDeleteBranches=List, create, or delete branches
|
||||
usage_logAllPretty=format:%H %ct %P' output=log --all '--pretty=format:%H %ct %P' output
|
||||
usage_moveRenameABranch=move/rename a branch
|
||||
usage_nameStatus=show only name and status of files
|
||||
usage_outputFile=Output file
|
||||
usage_path=path
|
||||
usage_performFsckStyleChecksOnReceive=perform fsck style checks on receive
|
||||
|
@ -170,6 +173,7 @@ usage_recurseIntoSubtrees=recurse into subtrees
|
|||
usage_recordChangesToRepository=Record changes to the repository
|
||||
usage_setTheGitRepositoryToOperateOn=set the git repository to operate on
|
||||
usage_showRefNamesMatchingCommits=Show ref names matching commits
|
||||
usage_showPatch=display patch
|
||||
usage_symbolicVersionForTheProject=Symbolic version for the project
|
||||
usage_synchronizeIPZillaData=Synchronize IPZilla data
|
||||
usage_tagMessage=tag message
|
||||
|
|
|
@ -45,28 +45,28 @@
|
|||
|
||||
package org.eclipse.jgit.pgm;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.PrintStream;
|
||||
import java.io.PrintWriter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import org.kohsuke.args4j.Argument;
|
||||
import org.kohsuke.args4j.Option;
|
||||
|
||||
import org.eclipse.jgit.diff.DiffEntry;
|
||||
import org.eclipse.jgit.diff.DiffFormatter;
|
||||
import org.eclipse.jgit.diff.MyersDiff;
|
||||
import org.eclipse.jgit.diff.RawText;
|
||||
import org.eclipse.jgit.diff.RawTextIgnoreAllWhitespace;
|
||||
import org.eclipse.jgit.diff.RawTextIgnoreLeadingWhitespace;
|
||||
import org.eclipse.jgit.diff.RawTextIgnoreTrailingWhitespace;
|
||||
import org.eclipse.jgit.diff.RawTextIgnoreWhitespaceChange;
|
||||
import org.eclipse.jgit.lib.FileMode;
|
||||
import org.eclipse.jgit.lib.ObjectId;
|
||||
import org.eclipse.jgit.diff.RenameDetector;
|
||||
import org.eclipse.jgit.lib.TextProgressMonitor;
|
||||
import org.eclipse.jgit.pgm.opt.PathTreeFilterHandler;
|
||||
import org.eclipse.jgit.treewalk.AbstractTreeIterator;
|
||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
|
||||
import org.eclipse.jgit.treewalk.filter.TreeFilter;
|
||||
import org.kohsuke.args4j.Argument;
|
||||
import org.kohsuke.args4j.Option;
|
||||
|
||||
@Command(common = true, usage = "usage_ShowDiffs")
|
||||
class Diff extends TextBuiltin {
|
||||
|
@ -78,9 +78,15 @@ void tree_0(final AbstractTreeIterator c) {
|
|||
@Argument(index = 1, metaVar = "metaVar_treeish", required = true)
|
||||
private final List<AbstractTreeIterator> trees = new ArrayList<AbstractTreeIterator>();
|
||||
|
||||
@Option(name = "--", metaVar = "metaVar_port", multiValued = true, handler = PathTreeFilterHandler.class)
|
||||
@Option(name = "--", metaVar = "metaVar_paths", multiValued = true, handler = PathTreeFilterHandler.class)
|
||||
private TreeFilter pathFilter = TreeFilter.ALL;
|
||||
|
||||
@Option(name = "-M", usage = "usage_detectRenames")
|
||||
private boolean detectRenames;
|
||||
|
||||
@Option(name = "--name-status", usage = "usage_nameStatus")
|
||||
private boolean showNameAndStatusOnly;
|
||||
|
||||
@Option(name = "--ignore-space-at-eol")
|
||||
private boolean ignoreWsTrailing;
|
||||
|
||||
|
@ -93,10 +99,69 @@ void tree_0(final AbstractTreeIterator c) {
|
|||
@Option(name = "-w", aliases = { "--ignore-all-space" })
|
||||
private boolean ignoreWsAll;
|
||||
|
||||
private DiffFormatter fmt = new DiffFormatter();
|
||||
@Option(name = "-U", aliases = { "--unified" }, metaVar = "metaVar_linesOfContext")
|
||||
void unified(int lines) {
|
||||
fmt.setContext(lines);
|
||||
}
|
||||
|
||||
private DiffFormatter fmt = new DiffFormatter() {
|
||||
@Override
|
||||
protected RawText newRawText(byte[] raw) {
|
||||
if (ignoreWsAll)
|
||||
return new RawTextIgnoreAllWhitespace(raw);
|
||||
else if (ignoreWsTrailing)
|
||||
return new RawTextIgnoreTrailingWhitespace(raw);
|
||||
else if (ignoreWsChange)
|
||||
return new RawTextIgnoreWhitespaceChange(raw);
|
||||
else if (ignoreWsLeading)
|
||||
return new RawTextIgnoreLeadingWhitespace(raw);
|
||||
else
|
||||
return new RawText(raw);
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void run() throws Exception {
|
||||
List<DiffEntry> files = scan();
|
||||
|
||||
if (showNameAndStatusOnly) {
|
||||
nameStatus(out, files);
|
||||
out.flush();
|
||||
|
||||
} else {
|
||||
BufferedOutputStream o = new BufferedOutputStream(System.out);
|
||||
fmt.format(o, db, files);
|
||||
o.flush();
|
||||
}
|
||||
}
|
||||
|
||||
static void nameStatus(PrintWriter out, List<DiffEntry> files) {
|
||||
for (DiffEntry ent : files) {
|
||||
switch (ent.getChangeType()) {
|
||||
case ADD:
|
||||
out.println("A\t" + ent.getNewName());
|
||||
break;
|
||||
case DELETE:
|
||||
out.println("D\t" + ent.getOldName());
|
||||
break;
|
||||
case MODIFY:
|
||||
out.println("M\t" + ent.getNewName());
|
||||
break;
|
||||
case COPY:
|
||||
out.format("C%1$03d\t%2$s\t%3$s", ent.getScore(), //
|
||||
ent.getOldName(), ent.getNewName());
|
||||
out.println();
|
||||
break;
|
||||
case RENAME:
|
||||
out.format("R%1$03d\t%2$s\t%3$s", ent.getScore(), //
|
||||
ent.getOldName(), ent.getNewName());
|
||||
out.println();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private List<DiffEntry> scan() throws IOException {
|
||||
final TreeWalk walk = new TreeWalk(db);
|
||||
walk.reset();
|
||||
walk.setRecursive(true);
|
||||
|
@ -104,65 +169,12 @@ protected void run() throws Exception {
|
|||
walk.addTree(i);
|
||||
walk.setFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, pathFilter));
|
||||
|
||||
while (walk.next())
|
||||
outputDiff(System.out, walk.getPathString(),
|
||||
walk.getObjectId(0), walk.getFileMode(0),
|
||||
walk.getObjectId(1), walk.getFileMode(1));
|
||||
}
|
||||
|
||||
protected void outputDiff(PrintStream out, String path,
|
||||
ObjectId id1, FileMode mode1, ObjectId id2, FileMode mode2) throws IOException {
|
||||
String name1 = "a/" + path;
|
||||
String name2 = "b/" + path;
|
||||
out.println("diff --git " + name1 + " " + name2);
|
||||
boolean isNew=false;
|
||||
boolean isDelete=false;
|
||||
if (id1.equals(ObjectId.zeroId())) {
|
||||
out.println("new file mode " + mode2);
|
||||
isNew=true;
|
||||
} else if (id2.equals(ObjectId.zeroId())) {
|
||||
out.println("deleted file mode " + mode1);
|
||||
isDelete=true;
|
||||
} else if (!mode1.equals(mode2)) {
|
||||
out.println("old mode " + mode1);
|
||||
out.println("new mode " + mode2);
|
||||
List<DiffEntry> files = DiffEntry.scan(walk);
|
||||
if (detectRenames) {
|
||||
RenameDetector rd = new RenameDetector(db);
|
||||
rd.addAll(files);
|
||||
files = rd.compute(new TextProgressMonitor());
|
||||
}
|
||||
out.println("index " + id1.abbreviate(db, 7).name()
|
||||
+ ".." + id2.abbreviate(db, 7).name()
|
||||
+ (mode1.equals(mode2) ? " " + mode1 : ""));
|
||||
out.println("--- " + (isNew ? "/dev/null" : name1));
|
||||
out.println("+++ " + (isDelete ? "/dev/null" : name2));
|
||||
|
||||
byte[] aRaw = getRawBytes(id1);
|
||||
byte[] bRaw = getRawBytes(id2);
|
||||
|
||||
if (RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
|
||||
out.println("Binary files differ");
|
||||
return;
|
||||
}
|
||||
|
||||
RawText a = getRawText(aRaw);
|
||||
RawText b = getRawText(bRaw);
|
||||
MyersDiff diff = new MyersDiff(a, b);
|
||||
fmt.formatEdits(out, a, b, diff.getEdits());
|
||||
}
|
||||
|
||||
private byte[] getRawBytes(ObjectId id) throws IOException {
|
||||
if (id.equals(ObjectId.zeroId()))
|
||||
return new byte[] {};
|
||||
return db.openBlob(id).getCachedBytes();
|
||||
}
|
||||
|
||||
private RawText getRawText(byte[] raw) {
|
||||
if (ignoreWsAll)
|
||||
return new RawTextIgnoreAllWhitespace(raw);
|
||||
else if (ignoreWsTrailing)
|
||||
return new RawTextIgnoreTrailingWhitespace(raw);
|
||||
else if (ignoreWsChange)
|
||||
return new RawTextIgnoreWhitespaceChange(raw);
|
||||
else if (ignoreWsLeading)
|
||||
return new RawTextIgnoreLeadingWhitespace(raw);
|
||||
else
|
||||
return new RawText(raw);
|
||||
return files;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -45,22 +45,32 @@
|
|||
|
||||
package org.eclipse.jgit.pgm;
|
||||
|
||||
import java.io.BufferedOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.text.DateFormat;
|
||||
import java.text.MessageFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Collection;
|
||||
import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import org.kohsuke.args4j.Option;
|
||||
import org.eclipse.jgit.diff.DiffEntry;
|
||||
import org.eclipse.jgit.diff.DiffFormatter;
|
||||
import org.eclipse.jgit.diff.RenameDetector;
|
||||
import org.eclipse.jgit.lib.AnyObjectId;
|
||||
import org.eclipse.jgit.lib.PersonIdent;
|
||||
import org.eclipse.jgit.lib.Ref;
|
||||
import org.eclipse.jgit.lib.TextProgressMonitor;
|
||||
import org.eclipse.jgit.revwalk.RevCommit;
|
||||
import org.eclipse.jgit.revwalk.RevWalk;
|
||||
import org.eclipse.jgit.treewalk.TreeWalk;
|
||||
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
|
||||
import org.eclipse.jgit.treewalk.filter.TreeFilter;
|
||||
import org.kohsuke.args4j.Option;
|
||||
|
||||
@Command(common = true, usage = "usage_viewCommitHistory")
|
||||
class Log extends RevWalkTextBuiltin {
|
||||
|
@ -73,6 +83,22 @@ class Log extends RevWalkTextBuiltin {
|
|||
@Option(name="--decorate", usage="usage_showRefNamesMatchingCommits")
|
||||
private boolean decorate;
|
||||
|
||||
@Option(name = "-M", usage = "usage_detectRenames")
|
||||
private boolean detectRenames;
|
||||
|
||||
@Option(name = "--name-status", usage = "usage_nameStatus")
|
||||
private boolean showNameAndStatusOnly;
|
||||
|
||||
@Option(name = "-p", usage = "usage_showPatch")
|
||||
private boolean showPatch;
|
||||
|
||||
@Option(name = "-U", aliases = { "--unified" }, metaVar = "metaVar_linesOfContext")
|
||||
void unified(int lines) {
|
||||
diffFmt.setContext(lines);
|
||||
}
|
||||
|
||||
private DiffFormatter diffFmt = new DiffFormatter();
|
||||
|
||||
Log() {
|
||||
fmt = new SimpleDateFormat("EEE MMM dd HH:mm:ss yyyy ZZZZZ", Locale.US);
|
||||
}
|
||||
|
@ -120,6 +146,34 @@ protected void show(final RevCommit c) throws Exception {
|
|||
}
|
||||
|
||||
out.println();
|
||||
if (c.getParentCount() > 0 && (showNameAndStatusOnly || showPatch))
|
||||
showDiff(c);
|
||||
out.flush();
|
||||
}
|
||||
|
||||
private void showDiff(RevCommit c) throws IOException {
|
||||
final TreeWalk tw = new TreeWalk(db);
|
||||
tw.reset();
|
||||
tw.setRecursive(true);
|
||||
tw.addTree(c.getParent(0).getTree());
|
||||
tw.addTree(c.getTree());
|
||||
tw.setFilter(AndTreeFilter.create(TreeFilter.ANY_DIFF, pathFilter));
|
||||
|
||||
List<DiffEntry> files = DiffEntry.scan(tw);
|
||||
if (detectRenames) {
|
||||
RenameDetector rd = new RenameDetector(db);
|
||||
rd.addAll(files);
|
||||
files = rd.compute(new TextProgressMonitor());
|
||||
}
|
||||
|
||||
if (showNameAndStatusOnly) {
|
||||
Diff.nameStatus(out, files);
|
||||
|
||||
} else {
|
||||
out.flush();
|
||||
BufferedOutputStream o = new BufferedOutputStream(System.out);
|
||||
diffFmt.format(o, db, files);
|
||||
o.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -114,7 +114,7 @@ void enableBoundary(final boolean on) {
|
|||
private final List<RevCommit> commits = new ArrayList<RevCommit>();
|
||||
|
||||
@Option(name = "--", metaVar = "metaVar_path", multiValued = true, handler = PathTreeFilterHandler.class)
|
||||
private TreeFilter pathFilter = TreeFilter.ALL;
|
||||
protected TreeFilter pathFilter = TreeFilter.ALL;
|
||||
|
||||
private final List<RevFilter> revLimiter = new ArrayList<RevFilter>();
|
||||
|
||||
|
|
|
@ -44,6 +44,7 @@
|
|||
|
||||
package org.eclipse.jgit.diff;
|
||||
|
||||
import static org.eclipse.jgit.lib.Constants.encode;
|
||||
import static org.eclipse.jgit.lib.Constants.encodeASCII;
|
||||
|
||||
import java.io.IOException;
|
||||
|
@ -51,7 +52,13 @@
|
|||
import java.util.List;
|
||||
|
||||
import org.eclipse.jgit.JGitText;
|
||||
import org.eclipse.jgit.lib.AbbreviatedObjectId;
|
||||
import org.eclipse.jgit.lib.Constants;
|
||||
import org.eclipse.jgit.lib.FileMode;
|
||||
import org.eclipse.jgit.lib.ObjectLoader;
|
||||
import org.eclipse.jgit.lib.Repository;
|
||||
import org.eclipse.jgit.patch.FileHeader;
|
||||
import org.eclipse.jgit.util.QuotedString;
|
||||
|
||||
/**
|
||||
* Format an {@link EditList} as a Git style unified patch script.
|
||||
|
@ -80,6 +87,159 @@ public void setContext(final int lineCount) {
|
|||
context = lineCount;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a patch script from a list of difference entries.
|
||||
*
|
||||
* @param out
|
||||
* stream to write the patch script out to.
|
||||
* @param src
|
||||
* repository the file contents can be read from.
|
||||
* @param entries
|
||||
* entries describing the affected files.
|
||||
* @throws IOException
|
||||
* a file's content cannot be read, or the output stream cannot
|
||||
* be written to.
|
||||
*/
|
||||
public void format(final OutputStream out, Repository src,
|
||||
List<? extends DiffEntry> entries) throws IOException {
|
||||
for(DiffEntry ent : entries) {
|
||||
if (ent instanceof FileHeader) {
|
||||
format(
|
||||
out,
|
||||
(FileHeader) ent, //
|
||||
newRawText(open(src, ent.getOldMode(), ent.getOldId())),
|
||||
newRawText(open(src, ent.getNewMode(), ent.getNewId())));
|
||||
} else {
|
||||
format(out, src, ent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void format(OutputStream out, Repository src, DiffEntry ent)
|
||||
throws IOException {
|
||||
String oldName = quotePath("a/" + ent.getOldName());
|
||||
String newName = quotePath("b/" + ent.getNewName());
|
||||
out.write(encode("diff --git " + oldName + " " + newName + "\n"));
|
||||
|
||||
switch(ent.getChangeType()) {
|
||||
case ADD:
|
||||
out.write(encodeASCII("new file mode "));
|
||||
ent.getNewMode().copyTo(out);
|
||||
out.write('\n');
|
||||
break;
|
||||
|
||||
case DELETE:
|
||||
out.write(encodeASCII("deleted file mode "));
|
||||
ent.getOldMode().copyTo(out);
|
||||
out.write('\n');
|
||||
break;
|
||||
|
||||
case RENAME:
|
||||
out.write(encode("similarity index " + ent.getScore() + "%"));
|
||||
out.write('\n');
|
||||
|
||||
out.write(encode("rename from " + quotePath(ent.getOldName())));
|
||||
out.write('\n');
|
||||
|
||||
out.write(encode("rename to " + quotePath(ent.getNewName())));
|
||||
out.write('\n');
|
||||
break;
|
||||
|
||||
case COPY:
|
||||
out.write(encode("similarity index " + ent.getScore() + "%"));
|
||||
out.write('\n');
|
||||
|
||||
out.write(encode("copy from " + quotePath(ent.getOldName())));
|
||||
out.write('\n');
|
||||
|
||||
out.write(encode("copy to " + quotePath(ent.getNewName())));
|
||||
out.write('\n');
|
||||
|
||||
if (!ent.getOldMode().equals(ent.getNewMode())) {
|
||||
out.write(encodeASCII("new file mode "));
|
||||
ent.getNewMode().copyTo(out);
|
||||
out.write('\n');
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
switch (ent.getChangeType()) {
|
||||
case RENAME:
|
||||
case MODIFY:
|
||||
if (!ent.getOldMode().equals(ent.getNewMode())) {
|
||||
out.write(encodeASCII("old mode "));
|
||||
ent.getOldMode().copyTo(out);
|
||||
out.write('\n');
|
||||
|
||||
out.write(encodeASCII("new mode "));
|
||||
ent.getNewMode().copyTo(out);
|
||||
out.write('\n');
|
||||
}
|
||||
}
|
||||
|
||||
out.write(encodeASCII("index " //
|
||||
+ format(src, ent.getOldId()) //
|
||||
+ ".." //
|
||||
+ format(src, ent.getNewId())));
|
||||
if (ent.getOldMode().equals(ent.getNewMode())) {
|
||||
out.write(' ');
|
||||
ent.getNewMode().copyTo(out);
|
||||
}
|
||||
out.write('\n');
|
||||
out.write(encode("--- " + oldName + '\n'));
|
||||
out.write(encode("+++ " + newName + '\n'));
|
||||
|
||||
byte[] aRaw = open(src, ent.getOldMode(), ent.getOldId());
|
||||
byte[] bRaw = open(src, ent.getNewMode(), ent.getNewId());
|
||||
|
||||
if (RawText.isBinary(aRaw) || RawText.isBinary(bRaw)) {
|
||||
out.write(encodeASCII("Binary files differ\n"));
|
||||
|
||||
} else {
|
||||
RawText a = newRawText(aRaw);
|
||||
RawText b = newRawText(bRaw);
|
||||
formatEdits(out, a, b, new MyersDiff(a, b).getEdits());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct a RawText sequence for use with {@link MyersDiff}.
|
||||
*
|
||||
* @param content
|
||||
* text to be compared.
|
||||
* @return the raw text instance to handle the content.
|
||||
*/
|
||||
protected RawText newRawText(byte[] content) {
|
||||
return new RawText(content);
|
||||
}
|
||||
|
||||
private String format(Repository db, AbbreviatedObjectId oldId) {
|
||||
if (oldId.isComplete())
|
||||
oldId = oldId.toObjectId().abbreviate(db, 8);
|
||||
return oldId.name();
|
||||
}
|
||||
|
||||
private static String quotePath(String name) {
|
||||
String q = QuotedString.GIT_PATH.quote(name);
|
||||
return ('"' + name + '"').equals(q) ? name : q;
|
||||
}
|
||||
|
||||
private byte[] open(Repository src, FileMode mode, AbbreviatedObjectId id)
|
||||
throws IOException {
|
||||
if (mode == FileMode.MISSING)
|
||||
return new byte[] {};
|
||||
|
||||
if (mode.getObjectType() != Constants.OBJ_BLOB)
|
||||
return new byte[] {};
|
||||
|
||||
if (id.isComplete()) {
|
||||
ObjectLoader ldr = src.openObject(id.toObjectId());
|
||||
return ldr.getCachedBytes();
|
||||
}
|
||||
|
||||
return new byte[] {};
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a patch script, reusing a previously parsed FileHeader.
|
||||
* <p>
|
||||
|
|
Loading…
Reference in New Issue