org.apache.zeppelin.notebook.repo.GitNotebookRepo.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.zeppelin.notebook.repo.GitNotebookRepo.java

Source

/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You 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 org.apache.zeppelin.notebook.repo;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.base.Joiner;
import com.google.common.collect.Lists;
import org.apache.zeppelin.conf.ZeppelinConfiguration;
import org.apache.zeppelin.notebook.Note;
import org.apache.zeppelin.user.AuthenticationInfo;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoHeadException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.dircache.DirCache;
import org.eclipse.jgit.internal.storage.file.FileRepository;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.util.Collection;
import java.util.List;

/**
 * NotebookRepo that hosts all the notebook FS in a single Git repo
 *
 * This impl intended to be simple and straightforward:
 *   - does not handle branches
 *   - only basic local git file repo, no remote Github push\pull. GitHub integration is
 *   implemented in @see {@link org.apache.zeppelin.notebook.repo.GitNotebookRepo}
 *
 *   TODO(bzz): add default .gitignore
 */
public class GitNotebookRepo extends VFSNotebookRepo implements NotebookRepoWithVersionControl {
    private static final Logger LOGGER = LoggerFactory.getLogger(GitNotebookRepo.class);

    private Git git;

    public GitNotebookRepo() {
        super();
    }

    @VisibleForTesting
    public GitNotebookRepo(ZeppelinConfiguration conf) throws IOException {
        this();
        init(conf);
    }

    @Override
    public void init(ZeppelinConfiguration conf) throws IOException {
        //TODO(zjffdu), it is weird that I can not call super.init directly here, as it would cause
        //AbstractMethodError
        this.conf = conf;
        setNotebookDirectory(conf.getNotebookDir());

        LOGGER.info("Opening a git repo at '{}'", this.rootNotebookFolder);
        Repository localRepo = new FileRepository(Joiner.on(File.separator).join(this.rootNotebookFolder, ".git"));
        if (!localRepo.getDirectory().exists()) {
            LOGGER.info("Git repo {} does not exist, creating a new one", localRepo.getDirectory());
            localRepo.create();
        }
        git = new Git(localRepo);
    }

    @Override
    public void move(String noteId, String notePath, String newNotePath, AuthenticationInfo subject)
            throws IOException {
        super.move(noteId, notePath, newNotePath, subject);
        String noteFileName = buildNoteFileName(noteId, notePath);
        String newNoteFileName = buildNoteFileName(noteId, newNotePath);
        git.rm().addFilepattern(noteFileName);
        git.add().addFilepattern(newNoteFileName);
        try {
            git.commit().setMessage("Move note " + noteId + " from " + noteFileName + " to " + newNoteFileName)
                    .call();
        } catch (GitAPIException e) {
            throw new IOException(e);
        }
    }

    @Override
    public void move(String folderPath, String newFolderPath, AuthenticationInfo subject) throws IOException {
        super.move(folderPath, newFolderPath, subject);
        git.rm().addFilepattern(folderPath.substring(1));
        git.add().addFilepattern(newFolderPath.substring(1));
        try {
            git.commit().setMessage("Move folder " + folderPath + " to " + newFolderPath).call();
        } catch (GitAPIException e) {
            throw new IOException(e);
        }
    }

    /* implemented as git add+commit
     * @param noteId is the noteId
     * @param noteName name of the note
     * @param commitMessage is a commit message (checkpoint message)
     * (non-Javadoc)
     * @see org.apache.zeppelin.notebook.repo.VFSNotebookRepo#checkpoint(String, String)
     */
    @Override
    public Revision checkpoint(String noteId, String notePath, String commitMessage, AuthenticationInfo subject)
            throws IOException {
        String noteFileName = buildNoteFileName(noteId, notePath);
        Revision revision = Revision.EMPTY;
        try {
            List<DiffEntry> gitDiff = git.diff().call();
            boolean modified = gitDiff.parallelStream()
                    .anyMatch(diffEntry -> diffEntry.getNewPath().equals(noteFileName));
            if (modified) {
                LOGGER.debug("Changes found for pattern '{}': {}", noteFileName, gitDiff);
                DirCache added = git.add().addFilepattern(noteFileName).call();
                LOGGER.debug("{} changes are about to be commited", added.getEntryCount());
                RevCommit commit = git.commit().setMessage(commitMessage).call();
                revision = new Revision(commit.getName(), commit.getShortMessage(), commit.getCommitTime());
            } else {
                LOGGER.debug("No changes found {}", noteFileName);
            }
        } catch (GitAPIException e) {
            LOGGER.error("Failed to add+commit {} to Git", noteFileName, e);
        }
        return revision;
    }

    /**
     * the idea is to:
     * 1. stash current changes
     * 2. remember head commit and checkout to the desired revision
     * 3. get note and checkout back to the head
     * 4. apply stash on top and remove it
     */
    @Override
    public synchronized Note get(String noteId, String notePath, String revId, AuthenticationInfo subject)
            throws IOException {
        Note note = null;
        RevCommit stash = null;
        String noteFileName = buildNoteFileName(noteId, notePath);
        try {
            List<DiffEntry> gitDiff = git.diff().setPathFilter(PathFilter.create(noteFileName)).call();
            boolean modified = !gitDiff.isEmpty();
            if (modified) {
                // stash changes
                stash = git.stashCreate().call();
                Collection<RevCommit> stashes = git.stashList().call();
                LOGGER.debug("Created stash : {}, stash size : {}", stash, stashes.size());
            }
            ObjectId head = git.getRepository().resolve(Constants.HEAD);
            // checkout to target revision
            git.checkout().setStartPoint(revId).addPath(noteFileName).call();
            // get the note
            note = super.get(noteId, notePath, subject);
            // checkout back to head
            git.checkout().setStartPoint(head.getName()).addPath(noteFileName).call();
            if (modified && stash != null) {
                // unstash changes
                ObjectId applied = git.stashApply().setStashRef(stash.getName()).call();
                ObjectId dropped = git.stashDrop().setStashRef(0).call();
                Collection<RevCommit> stashes = git.stashList().call();
                LOGGER.debug("Stash applied as : {}, and dropped : {}, stash size: {}", applied, dropped,
                        stashes.size());
            }
        } catch (GitAPIException e) {
            LOGGER.error("Failed to return note from revision \"{}\"", revId, e);
        }
        return note;
    }

    @Override
    public List<Revision> revisionHistory(String noteId, String notePath, AuthenticationInfo subject)
            throws IOException {
        List<Revision> history = Lists.newArrayList();
        String noteFileName = buildNoteFileName(noteId, notePath);
        LOGGER.debug("Listing history for {}:", noteFileName);
        try {
            Iterable<RevCommit> logs = git.log().addPath(noteFileName).call();
            for (RevCommit log : logs) {
                history.add(new Revision(log.getName(), log.getShortMessage(), log.getCommitTime()));
                LOGGER.debug(" - ({},{},{})", log.getName(), log.getCommitTime(), log.getFullMessage());
            }
        } catch (NoHeadException e) {
            //when no initial commit exists
            LOGGER.warn("No Head found for {}, {}", noteFileName, e.getMessage());
        } catch (GitAPIException e) {
            LOGGER.error("Failed to get logs for {}", noteFileName, e);
        }
        return history;
    }

    @Override
    public Note setNoteRevision(String noteId, String notePath, String revId, AuthenticationInfo subject)
            throws IOException {
        Note revisionNote = get(noteId, notePath, revId, subject);
        if (revisionNote != null) {
            save(revisionNote, subject);
        }
        return revisionNote;
    }

    @Override
    public void close() {
        git.getRepository().close();
    }

    //DI replacements for Tests
    protected Git getGit() {
        return git;
    }

    void setGit(Git git) {
        this.git = git;
    }

}