com.google.gerrit.server.git.VersionedMetaData.java Source code

Java tutorial

Introduction

Here is the source code for com.google.gerrit.server.git.VersionedMetaData.java

Source

// Copyright (C) 2010 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.google.gerrit.server.git;

import com.google.common.base.MoreObjects;
import com.google.common.collect.Lists;

import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.dircache.DirCacheBuilder;
import org.eclipse.jgit.dircache.DirCacheEditor;
import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
import org.eclipse.jgit.dircache.DirCacheEntry;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.errors.MissingObjectException;
import org.eclipse.jgit.lib.AnyObjectId;
import org.eclipse.jgit.lib.BatchRefUpdate;
import org.eclipse.jgit.lib.CommitBuilder;
import org.eclipse.jgit.lib.Config;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.FileMode;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectInserter;
import org.eclipse.jgit.lib.ObjectLoader;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.RefUpdate;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.treewalk.TreeWalk;
import org.eclipse.jgit.util.ChangeIdUtil;
import org.eclipse.jgit.util.RawParseUtils;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.StringReader;
import java.util.List;
import java.util.Objects;

/**
 * Support for metadata stored within a version controlled branch.
 * <p>
 * Implementors are responsible for supplying implementations of the onLoad and
 * onSave methods to read from the repository, or format an update that can
 * later be written back to the repository.
 */
public abstract class VersionedMetaData {
    /**
     * Path information that does not hold references to any repository
     * data structures, allowing the application to retain this object
     * for long periods of time.
     */
    public static class PathInfo {
        public final FileMode fileMode;
        public final String path;
        public final ObjectId objectId;

        protected PathInfo(TreeWalk tw) {
            fileMode = tw.getFileMode(0);
            path = tw.getPathString();
            objectId = tw.getObjectId(0);
        }
    }

    private RevCommit revision;
    protected ObjectReader reader;
    protected ObjectInserter inserter;
    protected DirCache newTree;

    /** @return name of the reference storing this configuration. */
    protected abstract String getRefName();

    /** Set up the metadata, parsing any state from the loaded revision. */
    protected abstract void onLoad() throws IOException, ConfigInvalidException;

    /**
     * Save any changes to the metadata in a commit.
     *
     * @return true if the commit should proceed, false to abort.
     * @throws IOException
     * @throws ConfigInvalidException
     */
    protected abstract boolean onSave(CommitBuilder commit) throws IOException, ConfigInvalidException;

    /** @return revision of the metadata that was loaded. */
    public ObjectId getRevision() {
        return revision != null ? revision.copy() : null;
    }

    /**
     * Load the current version from the branch.
     * <p>
     * The repository is not held after the call completes, allowing the
     * application to retain this object for long periods of time.
     *
     * @param db repository to access.
     * @throws IOException
     * @throws ConfigInvalidException
     */
    public void load(Repository db) throws IOException, ConfigInvalidException {
        Ref ref = db.getRefDatabase().exactRef(getRefName());
        load(db, ref != null ? ref.getObjectId() : null);
    }

    /**
     * Load a specific version from the repository.
     * <p>
     * This method is primarily useful for applying updates to a specific revision
     * that was shown to an end-user in the user interface. If there are conflicts
     * with another user's concurrent changes, these will be automatically
     * detected at commit time.
     * <p>
     * The repository is not held after the call completes, allowing the
     * application to retain this object for long periods of time.
     *
     * @param db repository to access.
     * @param id revision to load.
     * @throws IOException
     * @throws ConfigInvalidException
     */
    public void load(Repository db, ObjectId id) throws IOException, ConfigInvalidException {
        reader = db.newObjectReader();
        try {
            revision = id != null ? new RevWalk(reader).parseCommit(id) : null;
            onLoad();
        } finally {
            reader.close();
            reader = null;
        }
    }

    public void load(MetaDataUpdate update) throws IOException, ConfigInvalidException {
        load(update.getRepository());
    }

    public void load(MetaDataUpdate update, ObjectId id) throws IOException, ConfigInvalidException {
        load(update.getRepository(), id);
    }

    /**
     * Update this metadata branch, recording a new commit on its reference.
     *
     * @param update helper information to define the update that will occur.
     * @return the commit that was created
     * @throws IOException if there is a storage problem and the update cannot be
     *         executed as requested or if it failed because of a concurrent
     *         update to the same reference
     */
    public RevCommit commit(MetaDataUpdate update) throws IOException {
        BatchMetaDataUpdate batch = openUpdate(update);
        try {
            batch.write(update.getCommitBuilder());
            return batch.commit();
        } finally {
            batch.close();
        }
    }

    /**
     * Creates a new commit and a new ref based on this commit.
     *
     * @param update helper information to define the update that will occur.
     * @param refName name of the ref that should be created
     * @return the commit that was created
     * @throws IOException if there is a storage problem and the update cannot be
     *         executed as requested or if it failed because of a concurrent
     *         update to the same reference
     */
    public RevCommit commitToNewRef(MetaDataUpdate update, String refName) throws IOException {
        BatchMetaDataUpdate batch = openUpdate(update);
        try {
            batch.write(update.getCommitBuilder());
            return batch.createRef(refName);
        } finally {
            batch.close();
        }
    }

    public interface BatchMetaDataUpdate {
        void write(CommitBuilder commit) throws IOException;

        void write(VersionedMetaData config, CommitBuilder commit) throws IOException;

        RevCommit createRef(String refName) throws IOException;

        void removeRef(String refName) throws IOException;

        RevCommit commit() throws IOException;

        RevCommit commitAt(ObjectId revision) throws IOException;

        void close();
    }

    /**
     * Open a batch of updates to the same metadata ref.
     * <p>
     * This allows making multiple commits to a single metadata ref, at the end of
     * which is a single ref update. For batching together updates to multiple
     * refs (each consisting of one or more commits against their respective
     * refs), create the {@link MetaDataUpdate} with a {@link BatchRefUpdate}.
     * <p>
     * A ref update produced by this {@link BatchMetaDataUpdate} is only committed
     * if there is no associated {@link BatchRefUpdate}. As a result, the
     * configured ref updated event is not fired if there is an associated batch.
     *
     * @param update helper info about the update.
     * @throws IOException if the update failed.
     */
    public BatchMetaDataUpdate openUpdate(final MetaDataUpdate update) throws IOException {
        final Repository db = update.getRepository();

        reader = db.newObjectReader();
        inserter = db.newObjectInserter();
        final RevWalk rw = new RevWalk(reader);
        final RevTree tree = revision != null ? rw.parseTree(revision) : null;
        newTree = readTree(tree);
        return new BatchMetaDataUpdate() {
            AnyObjectId src = revision;
            AnyObjectId srcTree = tree;

            @Override
            public void write(CommitBuilder commit) throws IOException {
                write(VersionedMetaData.this, commit);
            }

            private boolean doSave(VersionedMetaData config, CommitBuilder commit) throws IOException {
                DirCache nt = config.newTree;
                ObjectReader r = config.reader;
                ObjectInserter i = config.inserter;
                try {
                    config.newTree = newTree;
                    config.reader = reader;
                    config.inserter = inserter;
                    return config.onSave(commit);
                } catch (ConfigInvalidException e) {
                    throw new IOException(
                            "Cannot update " + getRefName() + " in " + db.getDirectory() + ": " + e.getMessage(),
                            e);
                } finally {
                    config.newTree = nt;
                    config.reader = r;
                    config.inserter = i;
                }
            }

            @Override
            public void write(VersionedMetaData config, CommitBuilder commit) throws IOException {
                if (!doSave(config, commit)) {
                    return;
                }

                // Reuse tree from parent commit unless there are contents in newTree or
                // there is no tree for a parent commit.
                ObjectId res = newTree.getEntryCount() != 0 || srcTree == null ? newTree.writeTree(inserter)
                        : srcTree.copy();
                if (res.equals(srcTree) && !update.allowEmpty() && (commit.getTreeId() == null)) {
                    // If there are no changes to the content, don't create the commit.
                    return;
                }

                // If changes are made to the DirCache and those changes are written as
                // a commit and then the tree ID is set for the CommitBuilder, then
                // those previous DirCache changes will be ignored and the commit's
                // tree will be replaced with the ID in the CommitBuilder. The same is
                // true if you explicitly set tree ID in a commit and then make changes
                // to the DirCache; that tree ID will be ignored and replaced by that of
                // the tree for the updated DirCache.
                if (commit.getTreeId() == null) {
                    commit.setTreeId(res);
                } else {
                    // In this case, the caller populated the tree without using DirCache.
                    res = commit.getTreeId();
                }

                if (src != null) {
                    commit.addParentId(src);
                }

                if (update.insertChangeId()) {
                    ObjectId id = ChangeIdUtil.computeChangeId(res, getRevision(), commit.getAuthor(),
                            commit.getCommitter(), commit.getMessage());
                    commit.setMessage(ChangeIdUtil.insertId(commit.getMessage(), id));
                }

                src = inserter.insert(commit);
                srcTree = res;
            }

            @Override
            public RevCommit createRef(String refName) throws IOException {
                if (Objects.equals(src, revision)) {
                    return revision;
                }
                return updateRef(ObjectId.zeroId(), src, refName);
            }

            @Override
            public void removeRef(String refName) throws IOException {
                RefUpdate ru = db.updateRef(refName);
                ru.setForceUpdate(true);
                if (revision != null) {
                    ru.setExpectedOldObjectId(revision);
                }
                RefUpdate.Result result = ru.delete();
                switch (result) {
                case FORCED:
                    update.fireGitRefUpdatedEvent(ru);
                    return;
                default:
                    throw new IOException(
                            "Cannot delete " + ru.getName() + " in " + db.getDirectory() + ": " + ru.getResult());
                }
            }

            @Override
            public RevCommit commit() throws IOException {
                return commitAt(revision);
            }

            @Override
            public RevCommit commitAt(ObjectId expected) throws IOException {
                if (Objects.equals(src, expected)) {
                    return revision;
                }
                return updateRef(MoreObjects.firstNonNull(expected, ObjectId.zeroId()), src, getRefName());
            }

            @Override
            public void close() {
                newTree = null;

                rw.close();
                if (inserter != null) {
                    inserter.close();
                    inserter = null;
                }

                if (reader != null) {
                    reader.close();
                    reader = null;
                }
            }

            private RevCommit updateRef(AnyObjectId oldId, AnyObjectId newId, String refName) throws IOException {
                BatchRefUpdate bru = update.getBatch();
                if (bru != null) {
                    bru.addCommand(new ReceiveCommand(oldId.toObjectId(), newId.toObjectId(), refName));
                    inserter.flush();
                    revision = rw.parseCommit(newId);
                    return revision;
                }

                RefUpdate ru = db.updateRef(refName);
                ru.setExpectedOldObjectId(oldId);
                ru.setNewObjectId(src);
                ru.setRefLogIdent(update.getCommitBuilder().getAuthor());
                String message = update.getCommitBuilder().getMessage();
                if (message == null) {
                    message = "meta data update";
                }
                try (BufferedReader reader = new BufferedReader(new StringReader(message))) {
                    // read the subject line and use it as reflog message
                    ru.setRefLogMessage("commit: " + reader.readLine(), true);
                }
                inserter.flush();
                RefUpdate.Result result = ru.update();
                switch (result) {
                case NEW:
                case FAST_FORWARD:
                    revision = rw.parseCommit(ru.getNewObjectId());
                    update.fireGitRefUpdatedEvent(ru);
                    return revision;
                default:
                    throw new IOException(
                            "Cannot update " + ru.getName() + " in " + db.getDirectory() + ": " + ru.getResult());
                }
            }
        };
    }

    protected DirCache readTree(RevTree tree)
            throws IOException, MissingObjectException, IncorrectObjectTypeException {
        DirCache dc = DirCache.newInCore();
        if (tree != null) {
            DirCacheBuilder b = dc.builder();
            b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, tree);
            b.finish();
        }
        return dc;
    }

    protected Config readConfig(String fileName) throws IOException, ConfigInvalidException {
        Config rc = new Config();
        String text = readUTF8(fileName);
        if (!text.isEmpty()) {
            try {
                rc.fromText(text);
            } catch (ConfigInvalidException err) {
                throw new ConfigInvalidException(
                        "Invalid config file " + fileName + " in commit " + revision.name(), err);
            }
        }
        return rc;
    }

    protected String readUTF8(String fileName) throws IOException {
        byte[] raw = readFile(fileName);
        return raw.length != 0 ? RawParseUtils.decode(raw) : "";
    }

    protected byte[] readFile(String fileName) throws IOException {
        if (revision == null) {
            return new byte[] {};
        }

        TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree());
        if (tw != null) {
            ObjectLoader obj = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
            return obj.getCachedBytes(Integer.MAX_VALUE);

        } else {
            return new byte[] {};
        }
    }

    protected ObjectId getObjectId(String fileName) throws IOException {
        if (revision == null) {
            return null;
        }

        TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree());
        if (tw != null) {
            return tw.getObjectId(0);
        }

        return null;
    }

    public List<PathInfo> getPathInfos(boolean recursive) throws IOException {
        TreeWalk tw = new TreeWalk(reader);
        tw.addTree(revision.getTree());
        tw.setRecursive(recursive);
        List<PathInfo> paths = Lists.newArrayList();
        while (tw.next()) {
            paths.add(new PathInfo(tw));
        }
        return paths;
    }

    protected static void set(Config rc, String section, String subsection, String name, String value) {
        if (value != null) {
            rc.setString(section, subsection, name, value);
        } else {
            rc.unset(section, subsection, name);
        }
    }

    protected static void set(Config rc, String section, String subsection, String name, boolean value) {
        if (value) {
            rc.setBoolean(section, subsection, name, value);
        } else {
            rc.unset(section, subsection, name);
        }
    }

    protected static <E extends Enum<?>> void set(Config rc, String section, String subsection, String name,
            E value, E defaultValue) {
        if (value != defaultValue) {
            rc.setEnum(section, subsection, name, value);
        } else {
            rc.unset(section, subsection, name);
        }
    }

    protected void saveConfig(String fileName, Config cfg) throws IOException {
        saveUTF8(fileName, cfg.toText());
    }

    protected void saveUTF8(String fileName, String text) throws IOException {
        saveFile(fileName, text != null ? Constants.encode(text) : null);
    }

    protected void saveFile(String fileName, byte[] raw) throws IOException {
        DirCacheEditor editor = newTree.editor();
        if (raw != null && 0 < raw.length) {
            final ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, raw);
            editor.add(new PathEdit(fileName) {
                @Override
                public void apply(DirCacheEntry ent) {
                    ent.setFileMode(FileMode.REGULAR_FILE);
                    ent.setObjectId(blobId);
                }
            });
        } else {
            editor.add(new DeletePath(fileName));
        }
        editor.finish();
    }
}