org.commonjava.aprox.subsys.git.GitManager.java Source code

Java tutorial

Introduction

Here is the source code for org.commonjava.aprox.subsys.git.GitManager.java

Source

/**
 * Copyright (C) 2011 Red Hat, Inc. (jdcasey@commonjava.org)
 *
 * 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 org.commonjava.aprox.subsys.git;

import static org.apache.commons.lang.StringUtils.isEmpty;
import static org.apache.commons.lang.StringUtils.join;

import java.io.File;
import java.io.IOException;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.commonjava.aprox.audit.ChangeSummary;
import org.commonjava.maven.atlas.ident.util.JoinString;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.RmCommand;
import org.eclipse.jgit.api.Status;
import org.eclipse.jgit.api.errors.GitAPIException;
import org.eclipse.jgit.api.errors.NoFilepatternException;
import org.eclipse.jgit.diff.DiffEntry;
import org.eclipse.jgit.diff.DiffFormatter;
import org.eclipse.jgit.errors.NoWorkTreeException;
import org.eclipse.jgit.errors.RevisionSyntaxException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.IndexDiff.StageState;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.ObjectReader;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revplot.PlotCommit;
import org.eclipse.jgit.revplot.PlotCommitList;
import org.eclipse.jgit.revplot.PlotLane;
import org.eclipse.jgit.revplot.PlotWalk;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevTree;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.storage.file.FileRepositoryBuilder;
import org.eclipse.jgit.treewalk.CanonicalTreeParser;
import org.eclipse.jgit.treewalk.FileTreeIterator;
import org.eclipse.jgit.treewalk.filter.AndTreeFilter;
import org.eclipse.jgit.treewalk.filter.PathFilter;
import org.eclipse.jgit.treewalk.filter.TreeFilter;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class GitManager {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    private final Git git;

    private final String email;

    private final Repository repo;

    private final File rootDir;

    private final GitConfig config;

    public GitManager(final GitConfig config) throws GitSubsystemException {
        this.config = config;
        rootDir = config.getContentDir();
        final String cloneUrl = config.getCloneFrom();

        boolean checkUpdate = false;
        if (cloneUrl != null) {
            logger.info("Cloning: {} into: {}", cloneUrl, rootDir);
            if (rootDir.isDirectory()) {
                checkUpdate = true;
            } else {
                final boolean mkdirs = rootDir.mkdirs();
                logger.info("git dir {} (mkdir result: {}; is directory? {}) contains:\n  {}", rootDir, mkdirs,
                        rootDir.isDirectory(), join(rootDir.listFiles(), "\n  "));
                try {
                    Git.cloneRepository().setURI(cloneUrl).setDirectory(rootDir).setRemote("origin").call();
                } catch (final GitAPIException e) {
                    throw new GitSubsystemException("Failed to clone remote URL: {} into: {}. Reason: {}", e,
                            cloneUrl, rootDir, e.getMessage());
                }
            }
        }

        final File dotGitDir = new File(rootDir, ".git");

        logger.info("Setting up git manager for: {}", dotGitDir);
        try {
            repo = new FileRepositoryBuilder().readEnvironment().setGitDir(dotGitDir).build();
        } catch (final IOException e) {
            throw new GitSubsystemException("Failed to create Repository instance for: {}. Reason: {}", e,
                    dotGitDir, e.getMessage());
        }

        String[] preExistingFromCreate = null;
        if (!dotGitDir.isDirectory()) {
            preExistingFromCreate = rootDir.list();

            try {
                repo.create();
            } catch (final IOException e) {
                throw new GitSubsystemException("Failed to create git repo: {}. Reason: {}", e, rootDir,
                        e.getMessage());
            }
        }

        String originUrl = repo.getConfig().getString("remote", "origin", "url");
        if (originUrl == null) {
            originUrl = cloneUrl;
            logger.info("Setting origin URL: {}", originUrl);

            repo.getConfig().setString("remote", "origin", "url", originUrl);

            repo.getConfig().setString("remote", "origin", "fetch", "+refs/heads/*:refs/remotes/origin/*");
        }

        String email = repo.getConfig().getString("user", null, "email");

        if (email == null) {
            email = config.getUserEmail();
        }

        if (email == null) {
            try {
                email = "aprox@" + InetAddress.getLocalHost().getCanonicalHostName();

            } catch (final UnknownHostException e) {
                throw new GitSubsystemException("Failed to resolve 'localhost'. Reason: {}", e, e.getMessage());
            }
        }

        if (repo.getConfig().getString("user", null, "email") == null) {
            repo.getConfig().setString("user", null, "email", email);
        }

        this.email = email;

        git = new Git(repo);

        if (preExistingFromCreate != null && preExistingFromCreate.length > 0) {
            addAndCommitPaths(new ChangeSummary(ChangeSummary.SYSTEM_USER, "Committing pre-existing files."),
                    preExistingFromCreate);
        }

        if (checkUpdate) {
            pullUpdates();
        }
    }

    public GitManager addAndCommitFiles(final ChangeSummary summary, final File... files)
            throws GitSubsystemException {
        return addAndCommitFiles(summary, Arrays.asList(files));
    }

    public GitManager addAndCommitFiles(final ChangeSummary summary, final Collection<File> files)
            throws GitSubsystemException {
        final Set<String> paths = new HashSet<>();
        for (final File f : files) {
            final String path = relativize(f);

            if (path != null && path.length() > 0) {
                paths.add(path);
            }

        }

        return addAndCommitPaths(summary, paths);
    }

    private String relativize(final File f) {
        return Paths.get(rootDir.toURI()).relativize(Paths.get(f.toURI())).toString();
    }

    public GitManager addAndCommitPaths(final ChangeSummary summary, final String... paths)
            throws GitSubsystemException {
        return addAndCommitPaths(summary, Arrays.asList(paths));
    }

    public GitManager addAndCommitPaths(final ChangeSummary summary, final Collection<String> paths)
            throws GitSubsystemException {
        if (!verifyChangesExist(paths)) {
            logger.info("No actual changes in:\n  {}\n\nSkipping commit.", join(paths, "\n  "));
            return this;
        }

        try {
            final AddCommand add = git.add();
            final CommitCommand commit = git.commit();
            for (final String filepath : paths) {
                add.addFilepattern(filepath);
            }

            logger.info("Adding:\n  " + join(paths, "\n  ") + "\n\nSummary: " + summary);

            add.call();

            commit.setMessage(buildMessage(summary, paths)).setAuthor(summary.getUser(), email).call();
        } catch (final NoFilepatternException e) {
            throw new GitSubsystemException("Cannot add to git: " + e.getMessage(), e);
        } catch (final GitAPIException e) {
            throw new GitSubsystemException("Cannot add to git: " + e.getMessage(), e);
        }

        return this;
    }

    private boolean verifyChangesExist(final Collection<String> paths) throws GitSubsystemException {
        try {
            final DiffFormatter formatter = new DiffFormatter(System.out);
            formatter.setRepository(repo);

            // resolve the HEAD object
            final ObjectId oid = repo.resolve(Constants.HEAD);
            if (oid == null) {
                // if there's no head, then these must be real changes...
                return true;
            }

            // reset a new tree object to the HEAD
            final RevWalk walk = new RevWalk(repo);
            final RevCommit commit = walk.parseCommit(oid);
            final RevTree treeWalk = walk.parseTree(commit);

            // construct filters for the paths we're trying to add/commit
            final List<TreeFilter> filters = new ArrayList<>();
            for (final String path : paths) {
                filters.add(PathFilter.create(path));
            }

            // we're interested in trees with an actual diff. This should improve walk performance.
            filters.add(TreeFilter.ANY_DIFF);

            // set the path filters from above
            walk.setTreeFilter(AndTreeFilter.create(filters));

            // setup the tree for doing the comparison vs. uncommitted files
            final CanonicalTreeParser tree = new CanonicalTreeParser();
            final ObjectReader oldReader = repo.newObjectReader();
            try {
                tree.reset(oldReader, treeWalk.getId());
            } finally {
                oldReader.release();
            }
            walk.dispose();

            // this iterator will actually scan the uncommitted files for diff'ing
            final FileTreeIterator files = new FileTreeIterator(repo);

            // do the scan.
            final List<DiffEntry> entries = formatter.scan(tree, files);

            // we're not interested in WHAT the differences are, only that there are differences.
            return entries != null && !entries.isEmpty();
        } catch (final IOException e) {
            throw new GitSubsystemException("Failed to scan for actual changes among: %s. Reason: %s", e, paths,
                    e.getMessage());
        }
    }

    private String buildMessage(final ChangeSummary summary, final Collection<String> paths) {
        final StringBuilder message = new StringBuilder().append(summary.getSummary());
        if (config.isCommitFileManifestsEnabled()) {
            message.append("\n\nFiles changed:\n").append(join(paths, "\n"));

        }

        return message.toString();
    }

    public GitManager deleteAndCommit(final ChangeSummary summary, final File... deleted)
            throws GitSubsystemException {
        return deleteAndCommit(summary, Arrays.asList(deleted));
    }

    public GitManager deleteAndCommit(final ChangeSummary summary, final Collection<File> files)
            throws GitSubsystemException {
        final Set<String> paths = new HashSet<>();
        for (final File f : files) {
            final String path = relativize(f);

            if (path != null && path.length() > 0) {
                paths.add(path);
            }

        }

        return deleteAndCommitPaths(summary, paths);
    }

    public GitManager deleteAndCommitPaths(final ChangeSummary summary, final String... paths)
            throws GitSubsystemException {
        return deleteAndCommitPaths(summary, Arrays.asList(paths));
    }

    public GitManager deleteAndCommitPaths(final ChangeSummary summary, final Collection<String> paths)
            throws GitSubsystemException {
        try {
            RmCommand rm = git.rm();
            CommitCommand commit = git.commit();

            for (final String path : paths) {
                rm = rm.addFilepattern(path);
                commit = commit.setOnly(path);
            }

            logger.info("Deleting:\n  " + join(paths, "\n  ") + "\n\nSummary: " + summary);

            rm.call();

            commit.setMessage(buildMessage(summary, paths)).setAuthor(summary.getUser(), email).call();
        } catch (final NoFilepatternException e) {
            throw new GitSubsystemException("Cannot remove from git: " + e.getMessage(), e);
        } catch (final GitAPIException e) {
            throw new GitSubsystemException("Cannot remove from git: " + e.getMessage(), e);
        }

        return this;
    }

    public ChangeSummary getHeadCommit(final File f) throws GitSubsystemException {
        try {
            final ObjectId oid = repo.resolve("HEAD");

            final PlotWalk pw = new PlotWalk(repo);
            final RevCommit rc = pw.parseCommit(oid);
            pw.markStart(rc);

            final String filepath = relativize(f);

            pw.setTreeFilter(AndTreeFilter.create(PathFilter.create(filepath), TreeFilter.ANY_DIFF));

            final PlotCommitList<PlotLane> cl = new PlotCommitList<>();
            cl.source(pw);
            cl.fillTo(1);

            final PlotCommit<PlotLane> commit = cl.get(0);

            return toChangeSummary(commit);
        } catch (RevisionSyntaxException | IOException e) {
            throw new GitSubsystemException("Failed to resolve HEAD commit for: %s. Reason: %s", e, f,
                    e.getMessage());
        }
    }

    public List<ChangeSummary> getChangelog(final File f, final int start, final int length)
            throws GitSubsystemException {
        if (length == 0) {
            return Collections.emptyList();
        }

        try {
            final ObjectId oid = repo.resolve(Constants.HEAD);

            final PlotWalk pw = new PlotWalk(repo);
            final RevCommit rc = pw.parseCommit(oid);
            toChangeSummary(rc);
            pw.markStart(rc);

            final String filepath = relativize(f);
            logger.info("Getting changelog for: {} (start: {}, length: {})", filepath, start, length);

            if (!isEmpty(filepath) && !filepath.equals("/")) {
                pw.setTreeFilter(AndTreeFilter.create(PathFilter.create(filepath), TreeFilter.ANY_DIFF));
            } else {
                pw.setTreeFilter(TreeFilter.ANY_DIFF);
            }

            final List<ChangeSummary> changelogs = new ArrayList<ChangeSummary>();
            int count = 0;
            final int stop = length > 0 ? length + 1 : 0;
            RevCommit commit = null;
            while ((commit = pw.next()) != null && (stop < 1 || count < stop)) {
                if (count < start) {
                    count++;
                    continue;
                }

                //                printFiles( commit );
                changelogs.add(toChangeSummary(commit));
                count++;
            }

            if (length < -1) {
                final int remove = (-1 * length) - 1;
                for (int i = 0; i < remove; i++) {
                    changelogs.remove(changelogs.size() - 1);
                }
            }

            return changelogs;
        } catch (RevisionSyntaxException | IOException e) {
            throw new GitSubsystemException("Failed to resolve HEAD commit for: %s. Reason: %s", e, f,
                    e.getMessage());
        }
    }

    //    private void printFiles( final RevCommit commit )
    //        throws IOException
    //    {
    //        final RevWalk tree = new RevWalk( repo );
    //        final RevCommit parent = commit.getParentCount() > 0 ? tree.parseCommit( commit.getParent( 0 )
    //                                                                                       .getId() ) : null;
    //
    //        final DiffFormatter df = new DiffFormatter( DisabledOutputStream.INSTANCE );
    //        df.setRepository( repo );
    //        df.setDiffComparator( RawTextComparator.DEFAULT );
    //        df.setDetectRenames( true );
    //
    //        final List<DiffEntry> diffs;
    //        if ( parent == null )
    //        {
    //            diffs =
    //                df.scan( new EmptyTreeIterator(),
    //                         new CanonicalTreeParser( null, tree.getObjectReader(), commit.getTree() ) );
    //        }
    //        else
    //        {
    //            diffs = df.scan( parent.getTree(), commit.getTree() );
    //        }
    //
    //        for ( final DiffEntry diff : diffs )
    //        {
    //            logger.info( "({} {} {}", diff.getChangeType()
    //                                          .name(), diff.getNewMode()
    //                                                       .getBits(), diff.getNewPath() );
    //        }
    //    }

    private ChangeSummary toChangeSummary(final RevCommit commit) {
        final PersonIdent who = commit.getAuthorIdent();
        final Date when = new Date(TimeUnit.MILLISECONDS.convert(commit.getCommitTime(), TimeUnit.SECONDS));
        return new ChangeSummary(who.getName(), commit.getFullMessage(), when, commit.getId().name());
    }

    public GitManager pullUpdates() throws GitSubsystemException {
        return pullUpdates(ConflictStrategy.merge);
    }

    public GitManager pullUpdates(final ConflictStrategy strategy) throws GitSubsystemException {
        try {
            git.pull().setStrategy(strategy.mergeStrategy()).setRemoteBranchName(config.getRemoteBranchName())
                    .setRebase(true).call();
        } catch (final GitAPIException e) {
            throw new GitSubsystemException("Cannot pull content updates via git: " + e.getMessage(), e);
        }

        return this;
    }

    public GitManager pushUpdates() throws GitSubsystemException {
        try {
            git.push().call();
        } catch (final GitAPIException e) {
            throw new GitSubsystemException("Cannot push content updates via git: " + e.getMessage(), e);
        }

        return this;
    }

    public String getOriginUrl() {
        return git.getRepository().getConfig().getString("remote", "origin", "url");
    }

    public GitManager commitModifiedFiles(final ChangeSummary changeSummary) throws GitSubsystemException {
        Status status;
        try {
            status = git.status().call();
        } catch (NoWorkTreeException | GitAPIException e) {
            throw new GitSubsystemException("Failed to retrieve status of: %s. Reason: %s", e, rootDir,
                    e.getMessage());
        }

        final Map<String, StageState> css = status.getConflictingStageState();
        if (!css.isEmpty()) {
            throw new GitSubsystemException("%s contains conflicts. Cannot auto-commit.\n  %s", rootDir,
                    new JoinString("\n  ", css.entrySet()));
        }

        final Set<String> toAdd = new HashSet<>();
        final Set<String> modified = status.getModified();
        if (modified != null && !modified.isEmpty()) {
            toAdd.addAll(modified);
        }

        final Set<String> untracked = status.getUntracked();
        if (untracked != null && !untracked.isEmpty()) {
            toAdd.addAll(untracked);
        }

        final Set<String> untrackedFolders = status.getUntrackedFolders();
        if (untrackedFolders != null && !untrackedFolders.isEmpty()) {
            toAdd.addAll(untrackedFolders);

            //            for ( String folderPath : untrackedFolders )
            //            {
            //                File dir = new File( rootDir, folderPath );
            //                Files.walkFileTree( null, null )
            //            }
        }

        if (!toAdd.isEmpty()) {
            addAndCommitPaths(changeSummary, toAdd);
        }

        return this;
    }
}