/* 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.api.plumbing; import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; import java.util.List; import java.util.regex.Pattern; 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.RevObject; import org.locationtech.geogig.api.RevTag; import org.locationtech.geogig.api.RevTree; import com.google.common.base.Optional; /** * Resolves the reference given by a ref spec to the {@link ObjectId} it finally points to, * dereferencing symbolic refs as necessary. */ public class RevParse extends AbstractGeoGigOp<Optional<ObjectId>> { private static final char PARENT_DELIMITER = '^'; private static final char ANCESTOR_DELIMITER = '~'; private static final String PATH_SEPARATOR = ":"; private String refSpec; private static final Pattern HEX_PATTERN = Pattern.compile("^[0-9a-f]+$"); /** * @param refSpec the ref spec to resolve * @return {@code this} */ public RevParse setRefSpec(final String refSpec) { this.refSpec = refSpec; return this; } /** * Parses a geogig revision string and return an object id. * <p> * Combinations of these operators are supported: * <ul> * <li><b>HEAD</b>, <b>MERGE_HEAD</b>, <b>FETCH_HEAD</b></li> * <li><b>SHA-1</b>: a complete or abbreviated SHA-1</li> * <li><b>refs/...</b>: a complete reference name</li> * <li><b>short-name</b>: a short reference name under {@code refs/heads}, {@code refs/tags}, or * {@code refs/remotes} namespace</li> * <li><b>tag-NN-gABBREV</b>: output from describe, parsed by treating {@code ABBREV} as an * abbreviated SHA-1.</li> * <li><i>id</i><b>^</b>: first parent of commit <i>id</i>, this is the same as {@code id^1}</li> * <li><i>id</i><b>^0</b>: ensure <i>id</i> is a commit</li> * <li><i>id</i><b>^n</b>: n-th parent of commit <i>id</i></li> * <li><i>id</i><b>~n</b>: n-th historical ancestor of <i>id</i>, by first parent. {@code id~3} * is equivalent to {@code id^1^1^1} or {@code id^^^}.</li> * <li><i>id</i><b>:path</b>: Lookup path under tree named by <i>id</i></li> * <li><i>id</i><b>^{commit}</b>: ensure <i>id</i> is a commit</li> * <li><i>id</i><b>^{tree}</b>: ensure <i>id</i> is a tree</li> * <li><i>id</i><b>^{tag}</b>: ensure <i>id</i> is a tag</li> * <li><i>id</i><b>^{blob}</b>: ensure <i>id</i> is a blob</li> * </ul> * * <p> * The following operators are specified by Git conventions, but are not supported by this * method: * <ul> * <li><b>ref@{n}</b>: n-th version of ref as given by its reflog</li> * <li><b>ref@{time}</b>: value of ref at the designated time</li> * </ul> * * @throws IllegalArgumentException if the format of {@link #setRefSpec(String) refSpec} can't * be understood, it resolves to multiple objects (e.g. a partial object id that matches * multiple objects), or a precondition refSpec form was given (e.g. * <code> id^{commit}</code>) and the object {@code id} resolves to is not of the * expected type. * @return the resolved object id, may be {@link Optional#absent() absent} */ @Override protected Optional<ObjectId> _call() { checkState(this.refSpec != null, "refSpec was not given"); return revParse(this.refSpec); } private Optional<ObjectId> revParse(String refSpec) { String path = null; if (refSpec.contains(PATH_SEPARATOR)) { String[] tokens = refSpec.split(PATH_SEPARATOR); refSpec = tokens[0]; path = tokens[1]; } final String prefix; int parentN = -1;// -1 = not given, 0 = ensure id is a commit, >0 nth. parent (1 = first // parent, 2 = second parent, > 2 would resolve to absent as merge commits // have two parents) int ancestorN = -1;// -1 = not given, 0 = same commit, > 0 = nth. historical ancestor, by // first parent RevObject.TYPE type = null; final StringBuilder remaining = new StringBuilder(); if (refSpec.indexOf(PARENT_DELIMITER) > 0) { prefix = parsePrefix(refSpec, PARENT_DELIMITER); String suffix = parseSuffix(refSpec, PARENT_DELIMITER); if (suffix.indexOf('{') == 0) { type = parseType(suffix); } else { parentN = parseNumber(suffix, 1, remaining); } } else if (refSpec.indexOf(ANCESTOR_DELIMITER) > 0) { prefix = parsePrefix(refSpec, ANCESTOR_DELIMITER); String suffix = parseSuffix(refSpec, ANCESTOR_DELIMITER); ancestorN = parseNumber(suffix, 1, remaining); } else { prefix = refSpec; } Optional<ObjectId> resolved = resolveObject(prefix); if (!resolved.isPresent()) { return resolved; } if (parentN > -1) { resolved = resolveParent(resolved.get(), parentN); } else if (ancestorN > -1) { resolved = resolveAncestor(resolved.get(), ancestorN); } else if (type != null) { resolved = verifyId(resolved.get(), type); } if (resolved.isPresent() && remaining.length() > 0) { String newRefSpec = resolved.get().toString() + remaining.toString(); resolved = revParse(newRefSpec); } if (!resolved.isPresent()) { return resolved; } if (path != null) { NodeRef.checkValidPath(path); Optional<ObjectId> treeId = command(ResolveTreeish.class).setTreeish(resolved.get()) .call(); if (!treeId.isPresent() || treeId.get().isNull()) { return Optional.absent(); } Optional<RevTree> revTree = command(RevObjectParse.class).setObjectId(treeId.get()) .call(RevTree.class); Optional<NodeRef> ref = command(FindTreeChild.class).setParent(revTree.get()) .setChildPath(path).setIndex(true).call(); if (!ref.isPresent()) { return Optional.absent(); } resolved = Optional.of(ref.get().objectId()); } return resolved; } private Optional<ObjectId> resolveParent(final ObjectId objectId, final int parentN) { checkNotNull(objectId); checkArgument(parentN > -1); if (objectId.isNull()) { return Optional.absent(); } if (parentN == 0) { // 0 == check id is a commit Optional<RevObject> object = command(RevObjectParse.class).setObjectId(objectId).call(); checkArgument(object.isPresent() && object.get() instanceof RevCommit, "%s is not a commit: %s", objectId, (object.isPresent() ? object.get() .getType() : "null")); return Optional.of(objectId); } RevCommit commit = resolveCommit(objectId); if (parentN > commit.getParentIds().size()) { return Optional.absent(); } return commit.parentN(parentN - 1); } /** * @param objectId * @return */ private RevCommit resolveCommit(ObjectId objectId) { final Optional<RevObject> object = command(RevObjectParse.class).setObjectId(objectId) .call(); checkArgument(object.isPresent(), "No object named %s could be found", objectId); final RevObject revObject = object.get(); RevCommit commit; switch (revObject.getType()) { case COMMIT: commit = (RevCommit) revObject; break; case TAG: ObjectId commitId = ((RevTag) revObject).getCommitId(); commit = command(RevObjectParse.class).setObjectId(commitId).call(RevCommit.class) .get(); break; default: throw new IllegalArgumentException(String.format( "%s did not resolve to a commit or tag: %s", objectId, revObject.getType())); } return commit; } private Optional<ObjectId> resolveAncestor(ObjectId objectId, int ancestorN) { RevCommit commit; try { commit = resolveCommit(objectId); } catch (IllegalArgumentException e) { // This is throw when an ancestor exist, but is not in the repo in the case of a shallow // clone. We capture the eexception and return an Absent value, to have the same // behaviour // as in the case of parsing an ancestor that doesn't really exist. return Optional.absent(); } if (ancestorN == 0) { return Optional.of(commit.getId()); } Optional<ObjectId> firstParent = commit.parentN(0); if (!firstParent.isPresent()) { return Optional.absent(); } return resolveAncestor(firstParent.get(), ancestorN - 1); } private Optional<ObjectId> verifyId(ObjectId objectId, RevObject.TYPE type) { final Optional<RevObject> object = command(RevObjectParse.class).setObjectId(objectId) .call(); checkArgument(object.isPresent(), "No object named %s could be found", objectId); final RevObject revObject = object.get(); if (type.equals(revObject.getType())) { return Optional.of(revObject.getId()); } else { throw new IllegalArgumentException(String.format("%s did not resolve to %s: %s", objectId, type, revObject.getType())); } } private int parseNumber(final String suffix, final int defaultValue, StringBuilder remainingTarget) { if (suffix.isEmpty() || !Character.isDigit(suffix.charAt(0))) { remainingTarget.append(suffix); return defaultValue; } int i = 0; StringBuilder sb = new StringBuilder(); while (i < suffix.length() && Character.isDigit(suffix.charAt(i))) { sb.append(suffix.charAt(i)); i++; } remainingTarget.append(suffix.substring(i)); return Integer.parseInt(sb.toString()); } private String parseSuffix(final String spec, final char delim) { checkArgument(spec.indexOf(delim) > -1); String suffix = spec.substring(spec.indexOf(delim) + 1); return suffix; } private String parsePrefix(String spec, char delim) { checkArgument(spec.indexOf(delim) > -1); return spec.substring(0, spec.indexOf(delim)); } private RevObject.TYPE parseType(String spec) { String type = spec.substring(1, spec.length() - 1); if (type.equals("commit")) { return RevObject.TYPE.COMMIT; } else if (type.equals("tree")) { return RevObject.TYPE.TREE; } else if (type.equals("tag")) { return RevObject.TYPE.TAG; } else if (type.equals("feature")) { return RevObject.TYPE.FEATURE; } throw new IllegalArgumentException(String.format("%s did not resolve to a type", type)); } /** * @param objectName a ref name or object id */ private Optional<ObjectId> resolveObject(final String refSpec) { ObjectId resolvedTo = null; // is it a ref? Optional<Ref> ref = command(RefParse.class).setName(refSpec).call(); if (ref.isPresent()) { resolvedTo = ref.get().getObjectId(); } else { // does it look like an object id hash? boolean hexPatternMatches = HEX_PATTERN.matcher(refSpec).matches(); if (hexPatternMatches) { try { ObjectId parsed = ObjectId.valueOf(refSpec); if (parsed.isNull()) { return Optional.of(ObjectId.NULL); } if (parsed.equals(RevTree.EMPTY_TREE_ID)) { return Optional.of(RevTree.EMPTY_TREE_ID); } if (stagingDatabase().exists(parsed)) { return Optional.of(parsed); } } catch (IllegalArgumentException ignore) { // its a partial id } List<ObjectId> hashMatches = stagingDatabase().lookUp(refSpec); if (hashMatches.size() > 1) { throw new IllegalArgumentException(String.format( "Ref spec (%s) matches more than one object id: %s", refSpec, hashMatches.toString())); } if (hashMatches.size() == 1) { resolvedTo = hashMatches.get(0); } else if (ObjectId.NULL.toString().startsWith(refSpec)) { resolvedTo = ObjectId.NULL; } else if (RevTree.EMPTY_TREE_ID.toString().startsWith(refSpec)) { resolvedTo = RevTree.EMPTY.getId(); } } } return Optional.fromNullable(resolvedTo); } }