/*==========================================================================*\ | $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); }