/* Copyright (c) 2012-2014 Boundless and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Distribution License v1.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/org/documents/edl-v10.html
*
* Contributors:
* Gabriel Roldan (Boundless) - initial implementation
*/
package org.locationtech.geogig.cli.porcelain;
import java.io.IOException;
import java.util.Iterator;
import java.util.List;
import javax.annotation.Nullable;
import jline.console.ConsoleReader;
import org.fusesource.jansi.Ansi;
import org.geotools.geometry.jts.ReferencedEnvelope;
import org.geotools.referencing.CRS;
import org.locationtech.geogig.api.GeoGIG;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.plumbing.DiffBounds;
import org.locationtech.geogig.api.plumbing.DiffCount;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.api.plumbing.diff.DiffObjectCount;
import org.locationtech.geogig.api.plumbing.diff.DiffSummary;
import org.locationtech.geogig.api.porcelain.DiffOp;
import org.locationtech.geogig.cli.AbstractCommand;
import org.locationtech.geogig.cli.AnsiDecorator;
import org.locationtech.geogig.cli.CLICommand;
import org.locationtech.geogig.cli.GeogigCLI;
import org.locationtech.geogig.cli.InvalidParameterException;
import org.locationtech.geogig.cli.annotation.ReadOnly;
import org.opengis.geometry.BoundingBox;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import com.beust.jcommander.Parameter;
import com.beust.jcommander.Parameters;
import com.google.common.base.Optional;
import com.google.common.base.Strings;
import com.google.common.collect.Iterators;
import com.google.common.collect.Lists;
/**
* Shows changes between commits, commits and working tree, etc.
* <p>
* Usage:
* <ul>
* <li> {@code geogig diff [-- <path>...]}: compare working tree and index
* <li> {@code geogig diff <commit> [-- <path>...]}: compare the working tree with the given commit
* <li> {@code geogig diff --cached [-- <path>...]}: compare the index with the HEAD commit
* <li> {@code geogig diff --cached <commit> [-- <path>...]}: compare the index with the given commit
* <li> {@code geogig diff <commit1> <commit2> [-- <path>...]}: compare {@code commit1} with
* {@code commit2}, where {@code commit1} is the eldest or left side of the diff.
* </ul>
*
* @see DiffOp
*/
@ReadOnly
@Parameters(commandNames = "diff", commandDescription = "Show changes between commits, commit and working tree, etc")
public class Diff extends AbstractCommand implements CLICommand {
@Parameter(description = "[<commit> [<commit>]] [-- <path>...]", arity = 2)
private List<String> refSpec = Lists.newArrayList();
@Parameter(names = "--", hidden = true, variableArity = true)
private List<String> paths = Lists.newArrayList();
@Parameter(names = "--cached", description = "compares the specified tree (commit, branch, etc) and the staging area")
private boolean cached;
@Parameter(names = "--summary", description = "List only summary of changes")
private boolean summary;
@Parameter(names = "--nogeom", description = "Do not show detailed coordinate changes in geometries")
private boolean nogeom;
@Parameter(names = "--bounds", description = "Show only the bounds of the difference between the two trees")
private boolean bounds;
@Parameter(names = "--crs", description = "Coordinate reference system for --bounds (defaults to EPSG:4326 with lon/lat axis order)")
private String boundsCrs;
@Parameter(names = "--count", description = "Only count the number of changes between the two trees")
private boolean count;
/**
* Executes the diff command with the specified options.
*/
@Override
protected void runInternal(GeogigCLI cli) throws IOException {
checkParameter(refSpec.size() <= 2, "Commit list is too long :%s", refSpec);
checkParameter(!(nogeom && summary), "Only one printing mode allowed");
checkParameter(!(bounds && count), "Only one of --bounds or --count is allowed");
checkParameter(!(cached && refSpec.size() > 1),
"--cached allows zero or one ref specs to compare the index with.");
GeoGIG geogig = cli.getGeogig();
String oldVersion = resolveOldVersion();
String newVersion = resolveNewVersion();
List<String> paths = removeEmptyPaths();
if (bounds) {
DiffBounds diff = geogig.command(DiffBounds.class).setOldVersion(oldVersion)
.setNewVersion(newVersion).setCompareIndex(cached);
diff.setPathFilters(paths);
CoordinateReferenceSystem crs = parseCrs();
if (crs != null) {
diff.setCRS(crs);
}
DiffSummary<BoundingBox, BoundingBox> diffBounds = diff.call();
BoundsDiffPrinter.print(geogig, cli.getConsole(), diffBounds);
return;
}
if (count) {
if (oldVersion == null) {
oldVersion = Ref.HEAD;
}
if (newVersion == null) {
newVersion = cached ? Ref.STAGE_HEAD : Ref.WORK_HEAD;
}
DiffCount cdiff = geogig.command(DiffCount.class).setOldVersion(oldVersion)
.setNewVersion(newVersion);
cdiff.setFilter(paths);
DiffObjectCount count = cdiff.call();
ConsoleReader console = cli.getConsole();
console.println(String.format("Trees changed: %d, features changed: %,d",
count.treeCount(), count.featureCount()));
console.flush();
return;
}
DiffOp diff = geogig.command(DiffOp.class);
diff.setOldVersion(oldVersion).setNewVersion(newVersion).setCompareIndex(cached);
Iterator<DiffEntry> entries;
if (paths.isEmpty()) {
entries = diff.setProgressListener(cli.getProgressListener()).call();
} else {
entries = Iterators.emptyIterator();
for (String path : paths) {
Iterator<DiffEntry> moreEntries = diff.setFilter(path)
.setProgressListener(cli.getProgressListener()).call();
entries = Iterators.concat(entries, moreEntries);
}
}
if (!entries.hasNext()) {
cli.getConsole().println("No differences found");
return;
}
DiffPrinter printer;
if (summary) {
printer = new SummaryDiffPrinter();
} else {
printer = new FullDiffPrinter(nogeom, false);
}
DiffEntry entry;
while (entries.hasNext()) {
entry = entries.next();
printer.print(geogig, cli.getConsole(), entry);
}
}
private List<String> removeEmptyPaths() {
List<String> paths = Lists.newLinkedList(this.paths);
for (Iterator<String> it = paths.iterator(); it.hasNext();) {
if (Strings.isNullOrEmpty(it.next())) {
it.remove();
}
}
return paths;
}
private CoordinateReferenceSystem parseCrs() {
if (boundsCrs == null) {
return null;
}
try {
return CRS.decode(boundsCrs, true);
} catch (Exception e) {
throw new InvalidParameterException(String.format("Unrecognized CRS: '%s'", boundsCrs));
}
}
@Nullable
private String resolveOldVersion() {
return refSpec.size() > 0 ? refSpec.get(0) : null;
}
@Nullable
private String resolveNewVersion() {
return refSpec.size() > 1 ? refSpec.get(1) : null;
}
private static final class BoundsDiffPrinter {
public static void print(GeoGIG geogig, ConsoleReader console,
DiffSummary<BoundingBox, BoundingBox> diffBounds) throws IOException {
BoundingBox left = diffBounds.getLeft();
BoundingBox right = diffBounds.getRight();
Optional<BoundingBox> mergedResult = diffBounds.getMergedResult();
BoundingBox both = new ReferencedEnvelope();
if (mergedResult.isPresent()) {
both = mergedResult.get();
}
Ansi ansi = AnsiDecorator.newAnsi(console.getTerminal().isAnsiSupported());
ansi.a("left: ").a(bounds(left)).newline();
ansi.a("right: ").a(bounds(right)).newline();
ansi.a("both: ").a(bounds(both)).newline();
ansi.a("CRS: ").a(CRS.toSRS(left.getCoordinateReferenceSystem())).newline();
console.print(ansi.toString());
}
private static CharSequence bounds(BoundingBox b) {
if (b.isEmpty()) {
return "<empty>";
}
return String.format("%f,%f,%f,%f", b.getMinX(), b.getMinY(), b.getMaxX(), b.getMaxY());
}
}
}