Package org.locationtech.geogig.api.porcelain

Source Code of org.locationtech.geogig.api.porcelain.CommitOp

/* 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.porcelain;

import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import java.util.Collection;
import java.util.Date;
import java.util.LinkedList;
import java.util.List;

import javax.annotation.Nullable;

import org.locationtech.geogig.api.AbstractGeoGigOp;
import org.locationtech.geogig.api.CommitBuilder;
import org.locationtech.geogig.api.ObjectId;
import org.locationtech.geogig.api.Ref;
import org.locationtech.geogig.api.RevCommit;
import org.locationtech.geogig.api.RevPerson;
import org.locationtech.geogig.api.RevTree;
import org.locationtech.geogig.api.SymRef;
import org.locationtech.geogig.api.hooks.Hookable;
import org.locationtech.geogig.api.plumbing.RefParse;
import org.locationtech.geogig.api.plumbing.ResolveTreeish;
import org.locationtech.geogig.api.plumbing.RevObjectParse;
import org.locationtech.geogig.api.plumbing.UpdateRef;
import org.locationtech.geogig.api.plumbing.UpdateSymRef;
import org.locationtech.geogig.api.plumbing.WriteTree2;
import org.locationtech.geogig.api.plumbing.merge.ReadMergeCommitMessageOp;
import org.locationtech.geogig.storage.ObjectDatabase;

import com.google.common.base.Optional;
import com.google.common.base.Preconditions;
import com.google.common.base.Supplier;
import com.google.common.base.Suppliers;
import com.google.common.collect.Lists;

/**
* Commits the staged changed in the index to the repository, creating a new commit pointing to the
* new root tree resulting from moving the staged changes to the repository, and updating the HEAD
* ref to the new commit object.
* <p>
* Like {@code git commit -a}, If the {@link #setAll(boolean) all} flag is set, first stages all the
* changed objects in the index, but does not state newly created (unstaged) objects that are not
* already staged.
* </p>
*
*/
@Hookable(name = "commit")
public class CommitOp extends AbstractGeoGigOp<RevCommit> {

    private Optional<String> authorName;

    private Optional<String> authorEmail;

    private String message;

    private Long authorTimeStamp;

    private Long committerTimeStamp;

    private Integer committerTimeZoneOffset;

    private Integer authorTimeZoneOffset;

    private List<ObjectId> parents = new LinkedList<ObjectId>();

    // like the -a option in git commit
    private boolean all;

    private boolean allowEmpty;

    private String committerName;

    private String committerEmail;

    private RevCommit commit;

    private boolean amend;

    private final List<String> pathFilters = Lists.newLinkedList();

    /**
     * If set, ignores other information for creating a commit and uses the passed one.
     *
     * @param commit the commit to use
     *
     * @return {@code this}
     */
    public CommitOp setCommit(final RevCommit commit) {
        this.commit = commit;
        return this;
    }

    /**
     * If set, overrides the author's name from the configuration
     *
     * @param authorName the author's name
     * @param authorEmail the author's email
     * @return {@code this}
     */
    public CommitOp setAuthor(final @Nullable String authorName, @Nullable final String authorEmail) {
        this.authorName = Optional.fromNullable(authorName);
        this.authorEmail = Optional.fromNullable(authorEmail);
        return this;
    }

    /**
     * If set, overrides the committer's name from the configuration
     *
     * @param committerName the committer's name
     * @param committerEmail the committer's email
     */
    public CommitOp setCommitter(String committerName, @Nullable String committerEmail) {
        checkNotNull(committerName);
        this.committerName = committerName;
        this.committerEmail = committerEmail;
        return this;
    }

    /**
     * Sets the {@link RevCommit#getMessage() commit message}.
     *
     * @param message description of the changes to record the commit with.
     * @return {@code this}, to ease command chaining
     */
    public CommitOp setMessage(@Nullable final String message) {
        this.message = message;
        return this;
    }

    /**
     * Sets the {@link RevPerson#getTimestamp() timestamp} for the author of this commit, or if not
     * set defaults to the current system time at the time {@link #call()} is called.
     *
     * @param timestamp commit timestamp, in milliseconds, as in {@link Date#getTime()}
     * @return {@code this}, to ease command chaining
     */
    public CommitOp setAuthorTimestamp(@Nullable final Long timestamp) {
        this.authorTimeStamp = timestamp;
        return this;
    }

    /**
     * Sets the {@link RevPerson#getTimestamp() timestamp} for the committer of this commit, or if
     * not set defaults to the current system time at the time {@link #call()} is called.
     *
     * @param timestamp commit timestamp, in milliseconds, as in {@link Date#getTime()}
     * @return {@code this}, to ease command chaining
     */
    public CommitOp setCommitterTimestamp(@Nullable final Long timestamp) {
        this.committerTimeStamp = timestamp;
        return this;
    }

    /**
     * Sets the time zone offset of the author.
     *
     * @param timeZoneOffset time zone offset of the author
     * @return {@code this}, to ease command chaining
     */
    public CommitOp setAuthorTimeZoneOffset(@Nullable final Integer timeZoneOffset) {
        this.authorTimeZoneOffset = timeZoneOffset;
        return this;
    }

    /**
     * Sets the time zone offset of the committer.
     *
     * @param timeZoneOffset time zone offset of the committer
     * @return {@code this}, to ease command chaining
     */
    public CommitOp setCommitterTimeZoneOffset(@Nullable final Integer timeZoneOffset) {
        this.committerTimeZoneOffset = timeZoneOffset;
        return this;
    }

    /**
     * If {@code true}, tells {@link #call()} to stage all the unstaged changes that are not new
     * object before performing the commit.
     *
     * @param all {@code true} to stage changes before commit, {@code false} to not do that.
     *        Defaults to {@code false}.
     * @return {@code this}, to ease command chaining
     */
    public CommitOp setAll(boolean all) {
        this.all = all;
        return this;
    }

    public CommitOp setPathFilters(@Nullable List<String> pathFilters) {
        this.pathFilters.clear();
        if (pathFilters != null) {
            this.pathFilters.addAll(pathFilters);
        }
        return this;
    }

    /**
     * @param parents parents to add
     * @return {@code this}
     */
    public CommitOp addParents(Collection<ObjectId> parents) {
        this.parents.addAll(parents);
        return this;
    }

    /**
     * Sets whether the operation should ammend the last commit instead of creating a new one
     *
     * @param amend
     * @return
     */
    public CommitOp setAmend(boolean amend) {
        this.amend = amend;
        return this;
    }

    /**
     * Executes the commit operation.
     *
     * @return the commit just applied, or {@code null} if
     *         {@code getProgressListener().isCanceled()}
     * @see org.locationtech.geogig.api.AbstractGeoGigOp#call()
     * @throws NothingToCommitException if there are no staged changes by comparing the index
     *         staging tree and the repository HEAD tree.
     */
    @Override
    protected RevCommit _call() throws RuntimeException {
        final String committer = resolveCommitter();
        final String committerEmail = resolveCommitterEmail();
        final String author = resolveAuthor();
        final String authorEmail = resolveAuthorEmail();
        final Long authorTime = getAuthorTimeStamp();
        final Long committerTime = getCommitterTimeStamp();
        final Integer authorTimeZoneOffset = getAuthorTimeZoneOffset();
        final Integer committerTimeZoneOffset = getCommitterTimeZoneOffset();

        getProgressListener().started();
        float writeTreeProgress = 99f;
        if (all) {
            writeTreeProgress = 50f;
            AddOp op = command(AddOp.class);
            for (String st : pathFilters) {
                op.addPattern(st);
            }
            op.setUpdateOnly(true).setProgressListener(subProgress(49f)).call();
        }
        if (getProgressListener().isCanceled()) {
            return null;
        }

        final Optional<Ref> currHead = command(RefParse.class).setName(Ref.HEAD).call();
        checkState(currHead.isPresent(), "Repository has no HEAD, can't commit");
        final Ref headRef = currHead.get();
        checkState(headRef instanceof SymRef,//
                "HEAD is in a dettached state, cannot commit. Create a branch from it before committing");

        final String currentBranch = ((SymRef) headRef).getTarget();
        final ObjectId currHeadCommitId = headRef.getObjectId();

        Supplier<RevTree> oldRoot = resolveOldRoot();
        if (!currHeadCommitId.isNull()) {
            if (amend) {
                RevCommit headCommit = command(RevObjectParse.class).setObjectId(currHeadCommitId)
                        .call(RevCommit.class).get();
                parents.addAll(headCommit.getParentIds());
                if (message == null || message.isEmpty()) {
                    message = headCommit.getMessage();
                }
                RevTree commitTree = command(RevObjectParse.class)
                        .setObjectId(headCommit.getTreeId()).call(RevTree.class).get();
                oldRoot = Suppliers.ofInstance(commitTree);
            } else {
                parents.add(0, currHeadCommitId);
            }
        } else {
            Preconditions.checkArgument(!amend,
                    "Cannot amend. There is no previous commit to amend");
        }

        // additional operations in case we are committing after a conflicted merge
        final Optional<Ref> mergeHead = command(RefParse.class).setName(Ref.MERGE_HEAD).call();
        if (mergeHead.isPresent()) {
            ObjectId mergeCommitId = mergeHead.get().getObjectId();
            if (!mergeCommitId.isNull()) {
                parents.add(mergeCommitId);
            }
            if (message == null) {
                message = command(ReadMergeCommitMessageOp.class).call();
            }
        }

        ObjectId newTreeId;
        {
            WriteTree2 writeTree = command(WriteTree2.class);
            writeTree.setOldRoot(oldRoot).setProgressListener(subProgress(writeTreeProgress));
            if (!pathFilters.isEmpty()) {
                writeTree.setPathFilter(pathFilters);
            }
            newTreeId = writeTree.call();
        }

        if (getProgressListener().isCanceled()) {
            return null;
        }

        final ObjectId currentRootTreeId = command(ResolveTreeish.class)
                .setTreeish(currHeadCommitId).call().or(RevTree.EMPTY_TREE_ID);
        if (currentRootTreeId.equals(newTreeId)) {
            if (!allowEmpty) {
                throw new NothingToCommitException("Nothing to commit after " + currHeadCommitId);
            }
        }

        final RevCommit commit;
        if (this.commit == null) {
            CommitBuilder cb = new CommitBuilder();
            cb.setAuthor(author);
            cb.setAuthorEmail(authorEmail);
            cb.setCommitter(committer);
            cb.setCommitterEmail(committerEmail);
            cb.setMessage(message);
            cb.setParentIds(parents);
            cb.setTreeId(newTreeId);
            cb.setCommitterTimestamp(committerTime);
            cb.setAuthorTimestamp(authorTime);
            cb.setCommitterTimeZoneOffset(committerTimeZoneOffset);
            cb.setAuthorTimeZoneOffset(authorTimeZoneOffset);
            commit = cb.build();
        } else {
            CommitBuilder cb = new CommitBuilder(this.commit);
            cb.setParentIds(parents);
            cb.setTreeId(newTreeId);
            cb.setCommitterTimestamp(committerTime);
            cb.setCommitterTimeZoneOffset(committerTimeZoneOffset);
            if (message != null) {
                cb.setMessage(message);
            }
            commit = cb.build();
        }

        if (getProgressListener().isCanceled()) {
            return null;
        }
        final ObjectDatabase objectDb = objectDatabase();
        objectDb.put(commit);
        // set the HEAD pointing to the new commit
        final Optional<Ref> branchHead = command(UpdateRef.class).setName(currentBranch)
                .setNewValue(commit.getId()).call();
        checkState(commit.getId().equals(branchHead.get().getObjectId()));

        final Optional<Ref> newHead = command(UpdateSymRef.class).setName(Ref.HEAD)
                .setNewValue(currentBranch).call();

        checkState(currentBranch.equals(((SymRef) newHead.get()).getTarget()));

        Optional<ObjectId> treeId = command(ResolveTreeish.class).setTreeish(
                branchHead.get().getObjectId()).call();
        checkState(treeId.isPresent());
        checkState(newTreeId.equals(treeId.get()));

        getProgressListener().setProgress(100f);
        getProgressListener().complete();

        // TODO: maybe all this "heads cleaning" should be put in an independent operation
        if (mergeHead.isPresent()) {
            command(UpdateRef.class).setDelete(true).setName(Ref.MERGE_HEAD).call();
            command(UpdateRef.class).setDelete(true).setName(Ref.ORIG_HEAD).call();
        }
        final Optional<Ref> cherrypickHead = command(RefParse.class).setName(Ref.CHERRY_PICK_HEAD)
                .call();
        if (cherrypickHead.isPresent()) {
            command(UpdateRef.class).setDelete(true).setName(Ref.CHERRY_PICK_HEAD).call();
            command(UpdateRef.class).setDelete(true).setName(Ref.ORIG_HEAD).call();
        }

        return commit;
    }

    private Supplier<RevTree> resolveOldRoot() {
        Supplier<RevTree> supplier = new Supplier<RevTree>() {
            @Override
            public RevTree get() {
                Optional<ObjectId> head = command(ResolveTreeish.class).setTreeish(Ref.HEAD).call();
                if (!head.isPresent() || head.get().isNull()) {
                    return RevTree.EMPTY;
                }
                return command(RevObjectParse.class).setObjectId(head.get()).call(RevTree.class)
                        .get();
            }
        };
        return Suppliers.memoize(supplier);
    }

    /**
     * @return the timestamp to be used for the committer
     */
    public long getCommitterTimeStamp() {
        if (committerTimeStamp == null) {
            committerTimeStamp = platform().currentTimeMillis();
        }
        return committerTimeStamp.longValue();
    }

    /**
     * @return the time zone offset to be used for the committer
     */
    public int getCommitterTimeZoneOffset() {
        if (committerTimeZoneOffset == null) {
            committerTimeZoneOffset = platform().timeZoneOffset(getCommitterTimeStamp());
        }
        return committerTimeZoneOffset.intValue();
    }

    /**
     * @return the timestamp to be used for the author
     */
    public long getAuthorTimeStamp() {
        return authorTimeStamp == null ? getCommitterTimeStamp() : authorTimeStamp;
    }

    /**
     * @return the time zone offset to be used for the committer
     */
    public int getAuthorTimeZoneOffset() {
        if (authorTimeZoneOffset == null) {
            authorTimeZoneOffset = getCommitterTimeZoneOffset();
        }
        return authorTimeZoneOffset.intValue();
    }

    private String resolveCommitter() {
        if (committerName != null) {
            return committerName;
        }

        final String key = "user.name";
        Optional<String> name = command(ConfigGet.class).setName(key).call();

        checkState(
                name.isPresent(),
                "%s not found in config. Use geogig config [--global] %s <your name> to configure it.",
                key, key);

        return name.get();

    }

    private String resolveCommitterEmail() {
        if (committerEmail != null) {
            return committerEmail;
        }

        final String key = "user.email";
        Optional<String> email = command(ConfigGet.class).setName(key).call();

        checkState(
                email.isPresent(),
                "%s not found in config. Use geogig config [--global] %s <your email> to configure it.",
                key, key);

        return email.get();
    }

    private String resolveAuthor() {
        return authorName == null ? resolveCommitter() : authorName.orNull();
    }

    private String resolveAuthorEmail() {
        // only use provided authorEmail if authorName was provided
        return authorName == null ? resolveCommitterEmail() : authorEmail.orNull();
    }

    /**
     * @param allowEmptyCommit whether to allow a commit that represents no changes over its parent
     * @return {@code this}
     */
    public CommitOp setAllowEmpty(boolean allowEmptyCommit) {
        this.allowEmpty = allowEmptyCommit;
        return this;
    }

}
TOP

Related Classes of org.locationtech.geogig.api.porcelain.CommitOp

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.