net.polydawn.mdm.commands.MdmReleaseInitCommand.java Source code

Java tutorial

Introduction

Here is the source code for net.polydawn.mdm.commands.MdmReleaseInitCommand.java

Source

/*
 * Copyright 2012 - 2014 Eric Myhre <http://exultant.us>
 *
 * This file is part of mdm <https://github.com/heavenlyhash/mdm/>.
 *
 * mdm is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, version 3 of the License, or
 * (at your option) any later version.
 *
 * This program 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 Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program. If not, see <http://www.gnu.org/licenses/>.
 */

package net.polydawn.mdm.commands;

import static net.polydawn.mdm.Loco.inputPrompt;
import java.io.*;
import java.net.*;
import net.polydawn.mdm.*;
import net.polydawn.mdm.errors.*;
import net.sourceforge.argparse4j.inf.*;
import org.eclipse.jgit.api.*;
import org.eclipse.jgit.api.errors.*;
import org.eclipse.jgit.errors.*;
import org.eclipse.jgit.lib.*;
import org.eclipse.jgit.storage.file.*;
import org.eclipse.jgit.submodule.*;
import org.eclipse.jgit.treewalk.filter.*;
import us.exultant.ahs.iob.*;
import us.exultant.ahs.util.*;

public class MdmReleaseInitCommand extends MdmCommand {
    public MdmReleaseInitCommand(Repository repo) {
        super(repo);
    }

    public void parse(Namespace args) {
        name = args.getString("name");
        path = args.getString("repo");

        // check if we're in a repo root.  we'll suggest slightly different default values if we are, as well as generate a gitlink in this repo to the new release repo.
        asSubmodule = isInRepoRoot();

        // pick out the name, if not given.
        if (name == null) {
            String prompt = "what's the name of this project";
            String nameSuggest = new File(System.getProperty("user.dir")).getName();
            if (args.getBoolean("use_defaults"))
                name = nameSuggest;
            else if (asSubmodule)
                prompt += " [default: " + nameSuggest + "] ";
            else
                nameSuggest = null;

            while (name == null) {
                name = inputPrompt(os, prompt + "?");
                if (name.equals("") && nameSuggest != null)
                    name = nameSuggest;
                name = name.trim();
                if (name.equals(""))
                    name = null;
            }
        }

        // the rest of args parsing is only valid if we're making releases repo that's a submodule.
        if (!asSubmodule)
            return;

        // ask for remote url.
        remotePublicUrl = args.getString("remote_url");
        if (remotePublicUrl == null)
            if (args.getBoolean("use_defaults")) {
                String parentRemote = repo.getConfig().getString(ConfigConstants.CONFIG_REMOTE_SECTION, "origin",
                        ConfigConstants.CONFIG_KEY_URL);
                if (parentRemote == null)
                    parentRemote = System.getProperty("user.dir");
                remotePublicUrl = "../" + name + "-releases.git";
                try {
                    remotePublicUrl = new URI(parentRemote + "/").resolve(remotePublicUrl).toString();
                } catch (URISyntaxException e) {
                }
            } else
                remotePublicUrl = inputPrompt(os, "Configure a remote url where this repo will be accessible?\n"
                        + "This will be committed to the project's .gitmodules file, and so should be a publicly accessible url.\n"
                        + "remote url: ");
        remotePublicUrl = remotePublicUrl.trim();

        // and another.
        remotePublishUrl = args.getString("remote_publish_url");
        if (remotePublishUrl == null)
            if (args.getBoolean("use_defaults"))
                remotePublishUrl = remotePublicUrl;
            else
                remotePublishUrl = inputPrompt(os,
                        "Configure a remote url you'll use to push this repo when making releases?\n"
                                + "This will not be committed to the project; just set in your local config.\n"
                                + "remote url [leave blank to use the same public url]: ");
        remotePublishUrl = remotePublishUrl.trim();
        if (remotePublishUrl.equals(""))
            remotePublishUrl = remotePublicUrl;
    }

    public void validate() {
        // pick out path, if not given.
        if (path == null)
            if (asSubmodule) // if we are initializating the releases repo as a submodule, the default location to put that submodule is "./releases"
                path = "releases";
            else // if we're not a submodule, then the default is to make use of the current directory.
                path = ".";

        // if running on windows, flip their directory chars to normal slashes, since committing escaped backslashes to a gitmodules config file is almost certainly not what want
        path = (File.separatorChar != '/') ? path.replace(File.separatorChar, '/') : path;

        // normalize the path if necessary.  (jgit commit trips over relative paths.  and it's pretty weird for a submodule handle, too.)
        if (asSubmodule && path.startsWith("./"))
            path = path.substring(2);
    }

    String name;
    public String path;
    boolean asSubmodule;
    String remotePublicUrl;
    String remotePublishUrl;

    public MdmExitMessage call() throws IOException, MdmException {
        // check for clean working area.
        try {
            assertReleaseRepoAreaClean();
        } catch (MdmExitMessage e) {
            return e;
        }

        // okay!  make the new releases-repo.  put a first commit it in to avoid awkwardness.
        Repository releaserepo = makeReleaseRepo();
        makeReleaseRepoFoundingCommit(releaserepo);
        makeReleaseRepoInitBranch(releaserepo);

        // if we're not a submodule, we're now done here, otherwise, the rest of the work revolves around the parent repo.
        if (!asSubmodule)
            return new MdmExitMessage(":D", "releases repo initialized");

        // add the new releases-repo as a submodule to the project repo.
        try {
            writeParentGitmoduleConfig(repo);
        } catch (ConfigInvalidException e) {
            throw new MdmExitInvalidConfig(Constants.DOT_GIT_MODULES);
        }
        writeReleaseRepoConfig(releaserepo);
        makeParentRepoLinkCommit(repo);

        return new MdmExitMessage(":D", "releases repo and submodule initialized");
    }

    /**
     * Check that the releases area free of clutter.
     *
     * @throws MdmExitMessage
     *                 if the location intended for the release repo is not empty or
     *                 if there are other submodules in the git index for that
     *                 location.
     */
    void assertReleaseRepoAreaClean() throws IOException {
        File pathFile = new File(path).getCanonicalFile();
        if (asSubmodule && SubmoduleWalk.forIndex(repo).setFilter(PathFilter.create(path)).next())
            throw new MdmExitMessage(":I", "there's already a releases module!  No changes made.");
        if (pathFile.exists() && !pathFile.isDirectory() || new File(pathFile, ".git").exists())
            throw new MdmExitMessage(":(",
                    "something already exists at the location we want to initialize the releases repo.  clear it out and try again.");
    }

    /**
     * Initialize a new non-bare repository at {@link #path}.
     *
     * @return handle to the repository created.
     *
     * @throws MdmRepositoryIOException
     */
    Repository makeReleaseRepo() {
        try {
            Repository releaserepo = new RepositoryBuilder().setWorkTree(new File(path).getCanonicalFile()).build();
            releaserepo.create(false);
            return releaserepo;
        } catch (IOException e) {
            throw new MdmRepositoryIOException("create a release repo", true, path, e);
        }
    }

    /**
     * Create a text file stating the repository name and commit it. This creates a
     * root commit for history so that we can actually wield the repo.
     *
     * @param releaserepo
     */
    void makeReleaseRepoFoundingCommit(Repository releaserepo) {
        // write readme file
        try {
            IOForge.saveFile("This is the releases repo for " + name + ".\n",
                    new File(path, "README").getCanonicalFile());
        } catch (IOException e) {
            throw new MdmRepositoryIOException("create a release repo", true, path, e);
        }

        // add and commit
        String currentAction = "commit into the new releases repo";
        try {
            new Git(releaserepo).add().addFilepattern("README").call();
        } catch (NoFilepatternException e) {
            throw new MajorBug(e); // why would an api throw exceptions like this *checked*?
        } catch (GitAPIException e) {
            throw new MdmUnrecognizedError(e);
        }
        try {
            new Git(releaserepo).commit().setOnly("README").setMessage("initialize releases repo for " + name + ".")
                    .call();
        } catch (NoHeadException e) {
            throw new MdmConcurrentException(
                    new MdmRepositoryStateException(currentAction, releaserepo.getWorkTree().toString(), e));
        } catch (WrongRepositoryStateException e) {
            throw new MdmConcurrentException(
                    new MdmRepositoryStateException(currentAction, releaserepo.getWorkTree().toString(), e));
        } catch (UnmergedPathsException e) {
            throw new MdmConcurrentException(
                    new MdmRepositoryStateException(currentAction, releaserepo.getWorkTree().toString(), e));
        } catch (ConcurrentRefUpdateException e) {
            throw new MdmConcurrentException(e);
        } catch (NoMessageException e) {
            throw new MajorBug(e); // why would an api throw exceptions like this *checked*?
        } catch (GitAPIException e) {
            throw new MdmUnrecognizedError(e);
        }
    }

    /**
     * Label the current commit (which in context is the root commit) with the
     * 'mdm/init' branch.
     *
     * This branch has two roles; firstly, it is the metadata that is considered the
     * official declaration of this repo as a valid mdm releases repo; secondly, when
     * mdm is fetching a dependency, this essentially empty branch is used as a safe
     * default for the remote.origin.fetch configuration of the submodule.
     *
     * @param releaserepo
     */
    void makeReleaseRepoInitBranch(Repository releaserepo) {
        String currentAction = "create branches in the new releases repo";
        try {
            new Git(releaserepo).branchCreate().setName("mdm/init").call();
        } catch (RefAlreadyExistsException e) {
            throw new MdmConcurrentException(
                    new MdmRepositoryStateException(currentAction, releaserepo.getWorkTree().toString(), e));
        } catch (RefNotFoundException e) {
            throw new MdmConcurrentException(
                    new MdmRepositoryStateException(currentAction, releaserepo.getWorkTree().toString(), e));
        } catch (InvalidRefNameException e) {
            throw new MajorBug(e); // branch name is fixed at compile time here and is quite valid, thanks
        } catch (GitAPIException e) {
            throw new MdmUnrecognizedError(e);
        }
    }

    /**
     * Writes a new submodule block to the parentRepo's .gitmodules file declaring the
     * release repo, and initializes the local .git/config file to match.
     *
     * @param parentRepo
     * @throws IOException
     * @throws ConfigInvalidException
     */
    void writeParentGitmoduleConfig(Repository parentRepo) throws IOException, ConfigInvalidException {
        // write gitmodule config for the new submodule
        StoredConfig gitmodulesCfg = new FileBasedConfig(
                new File(parentRepo.getWorkTree(), Constants.DOT_GIT_MODULES), parentRepo.getFS());
        gitmodulesCfg.load();
        gitmodulesCfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, ConfigConstants.CONFIG_KEY_PATH,
                path);
        gitmodulesCfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, ConfigConstants.CONFIG_KEY_URL,
                remotePublicUrl);
        gitmodulesCfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path,
                MdmConfigConstants.Module.MODULE_TYPE.toString(), MdmModuleType.RELEASES.toString());
        gitmodulesCfg.setString(ConfigConstants.CONFIG_SUBMODULE_SECTION, path, ConfigConstants.CONFIG_KEY_UPDATE,
                "none");
        gitmodulesCfg.save();

        // initialize local parent repo config for the submodule
        MdmModuleRelease module = MdmModuleRelease.load(parentRepo, path, gitmodulesCfg);
        Plumbing.initLocalConfig(parentRepo, module);
        parentRepo.getConfig().save();
    }

    /**
     * Write origin and fetch config into the release module. Only performed when
     * operating in submodule mode, since otherwise we don't have any requirement to
     * request a value for {@link #remotePublishUrl}.
     *
     * @param releaserepo
     * @throws IOException
     */
    void writeReleaseRepoConfig(Repository releaserepo) throws IOException {
        releaserepo.getConfig().setString(ConfigConstants.CONFIG_REMOTE_SECTION, "origin",
                ConfigConstants.CONFIG_KEY_URL, remotePublishUrl);
        releaserepo.getConfig().setString(ConfigConstants.CONFIG_REMOTE_SECTION, "origin", "fetch",
                "+refs/heads/*:refs/remotes/origin/*");
        releaserepo.getConfig().save();
    }

    /**
     * Commits the release repo path and the gitmodules file.
     *
     * @param repo
     * @throws MdmRepositoryStateException
     * @throws NoWorkTreeException
     */
    void makeParentRepoLinkCommit(Repository repo) throws MdmRepositoryStateException {
        String currentAction = "commit a link to the new releases repo into the parent repo";
        try {
            new Git(repo).add().addFilepattern(path).addFilepattern(Constants.DOT_GIT_MODULES).call();
        } catch (NoFilepatternException e) {
            throw new MajorBug(e); // why would an api throw exceptions like this *checked*?
        } catch (GitAPIException e) {
            throw new MdmUnrecognizedError(e);
        }
        try {
            new Git(repo).commit().setOnly(path).setOnly(Constants.DOT_GIT_MODULES)
                    .setMessage("initialize releases repo for " + name + ".").call();
        } catch (NoHeadException e) {
            throw new MdmRepositoryStateException(currentAction, repo.getWorkTree().toString(), e);
        } catch (UnmergedPathsException e) {
            throw new MdmRepositoryStateException(currentAction, repo.getWorkTree().toString(), e);
        } catch (WrongRepositoryStateException e) {
            throw new MdmRepositoryStateException(currentAction, repo.getWorkTree().toString(), e);
        } catch (ConcurrentRefUpdateException e) {
            throw new MdmConcurrentException(e);
        } catch (NoMessageException e) {
            throw new MajorBug(e); // why would an api throw exceptions like this *checked*?
        } catch (GitAPIException e) {
            throw new MdmUnrecognizedError(e);
        }
    }
}