/* Copyright (c) 2013-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:
* Victor Olaya (Boundless) - initial implementation
*/
package org.locationtech.geogig.api.plumbing.merge;
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.Set;
import org.locationtech.geogig.api.AbstractGeoGigOp;
import org.locationtech.geogig.api.NodeRef;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.RevCommit;
import org.locationtech.geogig.api.RevFeature;
import org.locationtech.geogig.api.RevFeatureType;
import org.locationtech.geogig.api.RevObject;
import org.locationtech.geogig.api.RevObject.TYPE;
import org.locationtech.geogig.api.plumbing.DiffFeature;
import org.locationtech.geogig.api.plumbing.DiffTree;
import org.locationtech.geogig.api.plumbing.FindTreeChild;
import org.locationtech.geogig.api.plumbing.ResolveObjectType;
import org.locationtech.geogig.api.plumbing.RevObjectParse;
import org.locationtech.geogig.api.plumbing.diff.AttributeDiff;
import org.locationtech.geogig.api.plumbing.diff.DiffEntry;
import org.locationtech.geogig.api.plumbing.diff.FeatureDiff;
import org.locationtech.geogig.repository.DepthSearch;
import org.locationtech.geogig.repository.Repository;
import org.opengis.feature.type.PropertyDescriptor;
import com.google.common.base.Optional;
import com.google.common.base.Suppliers;
import com.google.common.collect.ImmutableList;
/**
* Reports conflicts between changes introduced by a given commit and the last commit of the current
* head. That should give information about whether the specified commit can be applied safely on
* the current branch without overwriting changes. It classifies the changes of the commit in
* conflicting or unconflicting, so they can be applied partially
*/
public class ReportCommitConflictsOp extends AbstractGeoGigOp<MergeScenarioReport> {
private RevCommit commit;
/**
* @param commit the commit with the changes to apply {@link RevCommit}
*/
public ReportCommitConflictsOp setCommit(RevCommit commit) {
this.commit = commit;
return this;
}
@Override
protected MergeScenarioReport _call() {
MergeScenarioReport report = new MergeScenarioReport();
ObjectId parentCommitId = ObjectId.NULL;
if (commit.getParentIds().size() > 0) {
parentCommitId = commit.getParentIds().get(0);
}
ObjectId parentTreeId = ObjectId.NULL;
Repository repository = repository();
if (repository.commitExists(parentCommitId)) {
parentTreeId = repository.getCommit(parentCommitId).getTreeId();
}
// get changes
Iterator<DiffEntry> diffs = command(DiffTree.class).setOldTree(parentTreeId)
.setNewTree(commit.getTreeId()).setReportTrees(true).call();
while (diffs.hasNext()) {
DiffEntry diff = diffs.next();
String path = diff.oldPath() == null ? diff.newPath() : diff.oldPath();
Optional<RevObject> obj = command(RevObjectParse.class).setRefSpec(
Ref.HEAD + ":" + path).call();
switch (diff.changeType()) {
case ADDED:
if (obj.isPresent()) {
TYPE type = command(ResolveObjectType.class).setObjectId(
diff.getNewObject().objectId()).call();
if (TYPE.TREE.equals(type)) {
NodeRef headVersion = command(FindTreeChild.class).setChildPath(path)
.setParent(repository.getOrCreateHeadTree()).call().get();
if (!headVersion.getMetadataId()
.equals(diff.getNewObject().getMetadataId())) {
report.addConflict(new Conflict(path, ObjectId.NULL, diff
.getNewObject().getMetadataId(), headVersion.getMetadataId()));
}
} else {
if (!obj.get().getId().equals(diff.newObjectId())) {
report.addConflict(new Conflict(path, ObjectId.NULL,
diff.newObjectId(), obj.get().getId()));
}
}
} else {
report.addUnconflicted(diff);
}
break;
case REMOVED:
if (obj.isPresent()) {
if (obj.get().getId().equals(diff.oldObjectId())) {
report.addUnconflicted(diff);
} else {
report.addConflict(new Conflict(path, diff.oldObjectId(), ObjectId.NULL,
obj.get().getId()));
}
}
break;
case MODIFIED:
TYPE type = command(ResolveObjectType.class).setObjectId(
diff.getNewObject().objectId()).call();
if (TYPE.TREE.equals(type)) {
// TODO:see how to do this. For now, we will pass any change as a conflicted
// one
if (!diff.isChange()) {
report.addUnconflicted(diff);
}
} else {
String refSpec = Ref.HEAD + ":" + path;
obj = command(RevObjectParse.class).setRefSpec(refSpec).call();
if (!obj.isPresent()) {
// git reports this as a conflict but does not mark as conflicted, just adds
// the missing file.
// We add it and consider it unconflicted
report.addUnconflicted(diff);
break;
}
RevFeature feature = (RevFeature) obj.get();
DepthSearch depthSearch = new DepthSearch(repository.objectDatabase());
Optional<NodeRef> noderef = depthSearch
.find(this.workingTree().getTree(), path);
RevFeatureType featureType = command(RevObjectParse.class)
.setObjectId(noderef.get().getMetadataId()).call(RevFeatureType.class)
.get();
ImmutableList<PropertyDescriptor> descriptors = featureType.sortedDescriptors();
FeatureDiff featureDiff = command(DiffFeature.class)
.setOldVersion(Suppliers.ofInstance(diff.getOldObject()))
.setNewVersion(Suppliers.ofInstance(diff.getNewObject())).call();
Set<Entry<PropertyDescriptor, AttributeDiff>> attrDiffs = featureDiff
.getDiffs().entrySet();
RevFeature newFeature = command(RevObjectParse.class)
.setObjectId(diff.newObjectId()).call(RevFeature.class).get();
boolean ok = true;
for (Iterator<Entry<PropertyDescriptor, AttributeDiff>> iterator = attrDiffs
.iterator(); iterator.hasNext() && ok;) {
Entry<PropertyDescriptor, AttributeDiff> entry = iterator.next();
AttributeDiff attrDiff = entry.getValue();
PropertyDescriptor descriptor = entry.getKey();
switch (attrDiff.getType()) {
case ADDED:
if (descriptors.contains(descriptor)) {
ok = false;
}
break;
case REMOVED:
case MODIFIED:
if (!descriptors.contains(descriptor)) {
ok = false;
break;
}
for (int i = 0; i < descriptors.size(); i++) {
if (descriptors.get(i).equals(descriptor)) {
Optional<Object> value = feature.getValues().get(i);
Optional<Object> newValue = newFeature.getValues().get(i);
if (!newValue.equals(value)) { // if it's going to end up
// setting the same value, it is
// compatible, so no need to
// check
if (!attrDiff.canBeAppliedOn(value)) {
ok = false;
}
break;
}
}
}
}
}
if (ok) {
report.addUnconflicted(diff);
} else {
report.addConflict(new Conflict(path, diff.oldObjectId(), diff
.newObjectId(), obj.get().getId()));
}
}
break;
}
}
return report;
}
}