org.webcat.core.git.GitUtilities.java Source code

Java tutorial

Introduction

Here is the source code for org.webcat.core.git.GitUtilities.java

Source

/*==========================================================================*\
 |  $Id: GitUtilities.java,v 1.8 2012/06/22 16:23:18 aallowat Exp $
 |*-------------------------------------------------------------------------*|
 |  Copyright (C) 2011 Virginia Tech
 |
 |  This file is part of Web-CAT.
 |
 |  Web-CAT is free software; you can redistribute it and/or modify
 |  it under the terms of the GNU Affero General Public License as published
 |  by the Free Software Foundation; either version 3 of the License, or
 |  (at your option) any later version.
 |
 |  Web-CAT is distributed in the hope that it will be useful,
 |  but WITHOUT ANY WARRANTY; without even the implied warranty of
 |  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 |  GNU General Public License for more details.
 |
 |  You should have received a copy of the GNU Affero General Public License
 |  along with Web-CAT; if not, see <http://www.gnu.org/licenses/>.
\*==========================================================================*/

package org.webcat.core.git;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import java.util.Timer;
import java.util.TimerTask;
import org.apache.log4j.Logger;
import org.eclipse.jgit.api.AddCommand;
import org.eclipse.jgit.api.CommitCommand;
import org.eclipse.jgit.api.Git;
import org.eclipse.jgit.api.InitCommand;
import org.eclipse.jgit.api.PushCommand;
import org.eclipse.jgit.api.errors.NoFilepatternException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.lib.RepositoryCache;
import org.eclipse.jgit.lib.RepositoryCache.FileKey;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.transport.RefSpec;
import org.eclipse.jgit.util.FS;
import org.webcat.core.Application;
import org.webcat.core.EOBase;
import org.webcat.core.FileUtilities;
import org.webcat.core.RepositoryProvider;
import org.webcat.core.User;
import com.webobjects.eocontrol.EOEnterpriseObject;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSComparator;
import com.webobjects.foundation.NSMutableArray;

//-------------------------------------------------------------------------
/**
 * This class provides utility methods for working with Git repositories and
 * objects.
 *
 * @author  Tony Allevato
 * @author  Last changed by $Author: aallowat $
 * @version $Revision: 1.8 $, $Date: 2012/06/22 16:23:18 $
 */
public class GitUtilities {
    //~ Constructors ..........................................................

    // ----------------------------------------------------------
    /**
     * Prevent instantiation.
     */
    private GitUtilities() {
        // Do nothing.
    }

    //~ Methods ...............................................................

    // ----------------------------------------------------------
    /**
     * Gets the Git repository that is located in the file store area for the
     * specified object, creating it if necessary.
     *
     * @param object the EO object whose file store contains the Git repository
     * @return the repository
     */
    /*package*/ static Repository repositoryForObject(EOBase object) {
        File fsDir = Application.wcApplication().repositoryPathForObject(object);
        File wcDir = Application.wcApplication().workingCopyPathForObject(object);

        Repository repository = null;

        if (fsDir != null) {
            File masterRef = new File(fsDir, "refs/heads/master");
            if (!masterRef.exists()) {
                // If the directory exists but we don't have a master ref, then
                // the repository is probably corrupted. Blow away the
                // directory, clear the repository cache, and then the code
                // below will re-create it for us.

                log.warn("Found the directory for the repository at " + fsDir.getAbsolutePath()
                        + ", but it appears to be " + "corrupt (no master ref). Re-creating it...");

                RepositoryCache.clear();
                FileUtilities.deleteDirectory(fsDir);
                fsDir.mkdirs();
            }

            try {
                try {
                    repository = RepositoryCache.open(FileKey.lenient(fsDir, FS.DETECTED), true);

                    if (repository == null) {
                        log.error("RepositoryCache.open returned null for " + "object " + object);
                    }
                } catch (RepositoryNotFoundException e) {
                    log.info("Creating repository at " + fsDir + " for the first time");

                    repository = setUpNewRepository(object, fsDir);
                } catch (Exception e) {
                    log.error("An exception occurred while trying to get the " + "repository for object " + object,
                            e);
                }
            } catch (IOException e) {
                log.error("An exception occurred while trying to get the " + "repository for object " + object, e);
            }
        }

        RepositoryInfo repoInfo = new RepositoryInfo();
        repoInfo.repositoryDir = fsDir;
        repoInfo.workingCopyDir = wcDir;
        repositoryInfos.put(repository, repoInfo);

        return repository;
    }

    // ----------------------------------------------------------
    /**
     * Gets a repository that represents a working copy of the specified
     * bare repository, creating it for the first time by cloning it if
     * necessary.
     *
     * @param repository the bare repository whose working copy should be
     *     retrieved
     * @param forcePull if true, always perform a pull from the bare repository
     *     to update it even if it already exists; if false, only pull when the
     *     working copy is first being created
     * @return a {@code Repository} object representing the working copy
     */
    public static Repository workingCopyForRepository(Repository repository, boolean forcePull) {
        RepositoryInfo repoInfo = repositoryInfos.get(repository);

        GitCloner cloner = new GitCloner(repoInfo.repositoryDir, repoInfo.workingCopyDir);

        Repository wcRepository = null;

        try {
            wcRepository = cloner.cloneRepository(forcePull);
        } catch (IOException e) {
            log.error("An exception occurred while trying to get the " + "working copy repository for repository "
                    + repoInfo.repositoryDir, e);
        }

        return wcRepository;
    }

    // ----------------------------------------------------------
    /**
     * Push any changes to the specified working copy to the main repository
     * associated with the working copy.
     *
     * @param workingCopy the working copy
     * @param user the user who is making the push
     * @param commitMessage the commit message to associate with the push
     */
    public static void pushWorkingCopy(Repository workingCopy, User user, String commitMessage) {
        synchronized (workingCopyPushTimers) {
            Timer timer = workingCopyPushTimers.get(workingCopy);

            if (timer != null) {
                timer.cancel();
                workingCopyPushTimers.remove(workingCopy);
            }

            timer = new Timer();
            PushWorkingCopyTask task = new PushWorkingCopyTask(workingCopy, user, commitMessage);
            timer.schedule(task, 5000);

            workingCopyPushTimers.put(workingCopy, timer);
        }
    }

    // ----------------------------------------------------------
    public static RevCommit pushWorkingCopyImmediately(Repository workingCopy, User user, String commitMessage) {
        return pushWorkingCopyImmediately(workingCopy, user.name_LF(), user.email(), commitMessage);
    }

    // ----------------------------------------------------------
    @SuppressWarnings("deprecation")
    public static RevCommit pushWorkingCopyImmediately(Repository workingCopy, String authorName,
            String emailAddress, String commitMessage) {
        try {
            boolean amend = false;

            GitRepository gitRepo = new GitRepository(workingCopy);
            GitRef ref = gitRepo.refWithName(Constants.R_HEADS + Constants.MASTER);
            NSArray<GitCommit> commits = ref.commits();

            if (commits != null && !commits.isEmpty()) {
                GitCommit commit = commits.objectAtIndex(0);
                if (commitMessage.equals(commit.shortMessage())
                        && commit.commitTime().timeIntervalSinceNow() > -3 * 60 * 60) {
                    amend = true;
                }
            }

            Git git = new Git(workingCopy);

            git.add().addFilepattern(".").setUpdate(false).call();

            RevCommit commit = git.commit().setAuthor(authorName, emailAddress)
                    .setCommitter(authorName, emailAddress).setMessage(commitMessage).setAmend(amend).call();

            RefSpec allHeadsSpec = new RefSpec().setForceUpdate(true).setSourceDestination(
                    Constants.R_HEADS + Constants.MASTER, Constants.R_HEADS + Constants.MASTER);

            git.push().setRefSpecs(allHeadsSpec).call();

            return commit;
        } catch (Exception e) {
            log.error("Error updating repository: ", e);
        }

        return null;
    }

    // ----------------------------------------------------------
    private static class PushWorkingCopyTask extends TimerTask {
        //~ Constructors ......................................................

        // ----------------------------------------------------------
        public PushWorkingCopyTask(Repository workingCopy, User user, String commitMessage) {
            this.workingCopy = workingCopy;
            this.authorName = user.name_LF();
            this.emailAddress = user.email();
            this.commitMessage = commitMessage;
        }

        //~ Methods ...........................................................

        // ----------------------------------------------------------
        @Override
        public void run() {
            synchronized (workingCopyPushTimers) {
                workingCopyPushTimers.remove(workingCopy);

                pushWorkingCopyImmediately(workingCopy, authorName, emailAddress, commitMessage);
            }
        }

        //~ Static/instance variables .........................................

        private Repository workingCopy;
        private String authorName;
        private String emailAddress;
        private String commitMessage;
    }

    // ----------------------------------------------------------
    /**
     * Sorts an array of {@link GitTreeEntry} objects with a case-insensitive
     * ascending sort by name.
     *
     * @param entries the array of entries to sort
     */
    public static void sortEntries(NSMutableArray<GitTreeEntry> entries) {
        try {
            entries.sortUsingComparator(new NSComparator() {
                @Override
                public int compare(Object _lhs, Object _rhs) {
                    GitTreeEntry lhs = (GitTreeEntry) _lhs;
                    GitTreeEntry rhs = (GitTreeEntry) _rhs;

                    if (lhs.isTree() && !rhs.isTree()) {
                        return -1;
                    } else if (rhs.isTree() && !lhs.isTree()) {
                        return 1;
                    } else {
                        return lhs.path().compareToIgnoreCase(rhs.path());
                    }
                }
            });
        } catch (NSComparator.ComparisonException e) {
            // Do nothing.
        }
    }

    // ----------------------------------------------------------
    /**
     * Sets up a new base Git repository for the specified object.
     *
     * @param object the object whose file store is desired
     * @param location the file system location for the repository
     * @return the newly created repository
     */
    private static Repository setUpNewRepository(EOEnterpriseObject object, File location) throws IOException {
        // This method performs the following actions to set up a new
        // repository:
        //
        // 1) Creates a new bare repository in the location requested by the
        //    method argument.
        // 2) Creates a temporary non-bare repository in the system temp
        //    directory, which is then configured to use the bare repository
        //    as a remote repository.
        // 3) Creates a README.txt file in the temporary repository's working
        //    directory, which contains a welcome message determined by the
        //    type of object that created the repo.
        // 4) Adds the README.txt to the repository, commits the change, and
        //    then pushes the changes to the bare repository.
        // 5) Finally, the temporary repository is deleted.
        //
        // This results in a usable bare repository being created, which a user
        // can now clone locally in order to manage their Web-CAT file store.

        // Create the bare repository.
        InitCommand init = new InitCommand();
        init.setDirectory(location);
        init.setBare(true);
        Repository bareRepository = init.call().getRepository();

        // Create the temporary repository.
        File tempRepoDir = File.createTempFile("newgitrepo", null);
        tempRepoDir.delete();
        tempRepoDir.mkdirs();

        init = new InitCommand();
        init.setDirectory(tempRepoDir);
        Repository tempRepository = init.call().getRepository();

        // Create the welcome files in the temporary repo.

        if (object instanceof RepositoryProvider) {
            RepositoryProvider provider = (RepositoryProvider) object;

            try {
                provider.initializeRepositoryContents(tempRepoDir);
            } catch (Exception e) {
                log.error(
                        "The following exception occurred when trying to "
                                + "initialize the repository contents at " + location.getAbsolutePath()
                                + ", but I'm continuing " + "anyway to ensure that the repository isn't corrupt.",
                        e);
            }
        }

        // Make sure we created at least one file, since we can't do much with
        // an empty repository. If the object didn't put anything in the
        // staging area, we'll just create a dummy README file.

        File[] files = tempRepoDir.listFiles();
        boolean foundFile = false;

        for (File file : files) {
            String name = file.getName();
            if (!".".equals(name) && !"..".equals(name) && !".git".equalsIgnoreCase(name)) {
                foundFile = true;
                break;
            }
        }

        if (!foundFile) {
            PrintWriter writer = new PrintWriter(new File(tempRepoDir, "README.txt"));
            writer.println("This readme file is provided so that the initial repository\n"
                    + "has some content. You may delete it when you push other files\n"
                    + "into the repository, if you wish.");
            writer.close();
        }

        // Create an appropriate default .gitignore file.
        PrintWriter writer = new PrintWriter(new File(tempRepoDir, ".gitignore"));
        writer.println("~*");
        writer.println("._*");
        writer.println(".TemporaryItems");
        writer.println(".DS_Store");
        writer.println("Thumbs.db");
        writer.close();

        // Add the files to the temporary repository.
        AddCommand add = new AddCommand(tempRepository);
        add.addFilepattern(".");
        add.setUpdate(false);

        try {
            add.call();
        } catch (NoFilepatternException e) {
            log.error("An exception occurred when adding the welcome files " + "to the repository: ", e);
            return bareRepository;
        }

        // Commit the changes.
        String email = Application.configurationProperties().getProperty("coreAdminEmail");
        CommitCommand commit = new Git(tempRepository).commit();
        commit.setAuthor("Web-CAT", email);
        commit.setCommitter("Web-CAT", email);
        commit.setMessage("Initial repository setup.");

        try {
            commit.call();
        } catch (Exception e) {
            log.error("An exception occurred when committing the welcome files " + "to the repository: ", e);
            return bareRepository;
        }

        // Push the changes to the bare repository.
        PushCommand push = new Git(tempRepository).push();
        @SuppressWarnings("deprecation")
        String url = location.toURL().toString();
        push.setRemote(url);
        push.setRefSpecs(new RefSpec("master"), new RefSpec("master"));

        try {
            push.call();
        } catch (Exception e) {
            log.error("An exception occurred when pushing the welcome files " + "to the repository: ", e);
            return bareRepository;
        }

        // Cleanup after ourselves.
        FileUtilities.deleteDirectory(tempRepoDir);

        return bareRepository;
    }

    //~ Inner classes .........................................................

    // ----------------------------------------------------------
    /**
     * Used to filter refs that should be returned by the
     * {@link #refsForRepository()} method.
     */
    public interface RefFilter {
        // ------------------------------------------------------
        public boolean accepts(Ref ref);
    }

    // ----------------------------------------------------------
    /**
     * A small class to hold a mapping from a bare repository location to its
     * working copy location.
     */
    private static class RepositoryInfo {
        public File repositoryDir;
        public File workingCopyDir;
    }

    //~ Static/instance variables .............................................

    private static Map<Repository, RepositoryInfo> repositoryInfos = new HashMap<Repository, RepositoryInfo>();
    private static Map<Repository, Timer> workingCopyPushTimers = new HashMap<Repository, Timer>();

    private static final Logger log = Logger.getLogger(GitUtilities.class);
}