TextBuiltin.java

  1. /*
  2.  * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
  3.  * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
  4.  *
  5.  * This program and the accompanying materials are made available under the
  6.  * terms of the Eclipse Distribution License v. 1.0 which is available at
  7.  * https://www.eclipse.org/org/documents/edl-v10.php.
  8.  *
  9.  * SPDX-License-Identifier: BSD-3-Clause
  10.  */

  11. package org.eclipse.jgit.pgm;

  12. import static java.nio.charset.StandardCharsets.UTF_8;
  13. import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_LOG_OUTPUT_ENCODING;
  14. import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_SECTION_I18N;
  15. import static org.eclipse.jgit.lib.Constants.R_HEADS;
  16. import static org.eclipse.jgit.lib.Constants.R_REMOTES;
  17. import static org.eclipse.jgit.lib.Constants.R_TAGS;

  18. import java.io.BufferedWriter;
  19. import java.io.FileDescriptor;
  20. import java.io.FileInputStream;
  21. import java.io.FileOutputStream;
  22. import java.io.IOException;
  23. import java.io.InputStream;
  24. import java.io.OutputStream;
  25. import java.io.OutputStreamWriter;
  26. import java.nio.charset.Charset;
  27. import java.text.MessageFormat;
  28. import java.util.ResourceBundle;

  29. import org.eclipse.jgit.lib.ObjectId;
  30. import org.eclipse.jgit.lib.Repository;
  31. import org.eclipse.jgit.pgm.internal.CLIText;
  32. import org.eclipse.jgit.pgm.internal.SshDriver;
  33. import org.eclipse.jgit.pgm.opt.CmdLineParser;
  34. import org.eclipse.jgit.revwalk.RevWalk;
  35. import org.eclipse.jgit.transport.JschConfigSessionFactory;
  36. import org.eclipse.jgit.transport.SshSessionFactory;
  37. import org.eclipse.jgit.transport.sshd.DefaultProxyDataFactory;
  38. import org.eclipse.jgit.transport.sshd.JGitKeyCache;
  39. import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
  40. import org.eclipse.jgit.util.io.ThrowingPrintWriter;
  41. import org.kohsuke.args4j.CmdLineException;
  42. import org.kohsuke.args4j.Option;

  43. /**
  44.  * Abstract command which can be invoked from the command line.
  45.  * <p>
  46.  * Commands are configured with a single "current" repository and then the
  47.  * {@link #execute(String[])} method is invoked with the arguments that appear
  48.  * on the command line after the command name.
  49.  * <p>
  50.  * Command constructors should perform as little work as possible as they may be
  51.  * invoked very early during process loading, and the command may not execute
  52.  * even though it was constructed.
  53.  */
  54. public abstract class TextBuiltin {
  55.     private String commandName;

  56.     @Option(name = "--help", usage = "usage_displayThisHelpText", aliases = { "-h" })
  57.     private boolean help;

  58.     @Option(name = "--ssh", usage = "usage_sshDriver")
  59.     private SshDriver sshDriver = SshDriver.APACHE;

  60.     /**
  61.      * Input stream, typically this is standard input.
  62.      *
  63.      * @since 3.4
  64.      */
  65.     protected InputStream ins;

  66.     /**
  67.      * Writer to output to, typically this is standard output.
  68.      *
  69.      * @since 2.2
  70.      */
  71.     protected ThrowingPrintWriter outw;

  72.     /**
  73.      * Stream to output to, typically this is standard output.
  74.      *
  75.      * @since 2.2
  76.      */
  77.     protected OutputStream outs;

  78.     /**
  79.      * Error writer, typically this is standard error.
  80.      *
  81.      * @since 3.4
  82.      */
  83.     protected ThrowingPrintWriter errw;

  84.     /**
  85.      * Error output stream, typically this is standard error.
  86.      *
  87.      * @since 3.4
  88.      */
  89.     protected OutputStream errs;

  90.     /** Git repository the command was invoked within. */
  91.     protected Repository db;

  92.     /** Directory supplied via --git-dir command line option. */
  93.     protected String gitdir;

  94.     /** RevWalk used during command line parsing, if it was required. */
  95.     protected RevWalk argWalk;

  96.     final void setCommandName(String name) {
  97.         commandName = name;
  98.     }

  99.     /**
  100.      * If this command requires a repository.
  101.      *
  102.      * @return true if {@link #db}/{@link #getRepository()} is required
  103.      */
  104.     protected boolean requiresRepository() {
  105.         return true;
  106.     }

  107.     /**
  108.      * Initializes the command to work with a repository, including setting the
  109.      * output and error streams.
  110.      *
  111.      * @param repository
  112.      *            the opened repository that the command should work on.
  113.      * @param gitDir
  114.      *            value of the {@code --git-dir} command line option, if
  115.      *            {@code repository} is null.
  116.      * @param input
  117.      *            input stream from which input will be read
  118.      * @param output
  119.      *            output stream to which output will be written
  120.      * @param error
  121.      *            error stream to which errors will be written
  122.      * @since 4.9
  123.      */
  124.     public void initRaw(final Repository repository, final String gitDir,
  125.             InputStream input, OutputStream output, OutputStream error) {
  126.         this.ins = input;
  127.         this.outs = output;
  128.         this.errs = error;
  129.         init(repository, gitDir);
  130.     }

  131.     /**
  132.      * Get the log output encoding specified in the repository's
  133.      * {@code i18n.logOutputEncoding} configuration.
  134.      *
  135.      * @param repository
  136.      *            the repository.
  137.      * @return Charset corresponding to {@code i18n.logOutputEncoding}, or
  138.      *         {@code UTF_8}.
  139.      */
  140.     private Charset getLogOutputEncodingCharset(Repository repository) {
  141.         if (repository != null) {
  142.             String logOutputEncoding = repository.getConfig().getString(
  143.                     CONFIG_SECTION_I18N, null, CONFIG_KEY_LOG_OUTPUT_ENCODING);
  144.             if (logOutputEncoding != null) {
  145.                 try {
  146.                     return Charset.forName(logOutputEncoding);
  147.                 } catch (IllegalArgumentException e) {
  148.                     throw die(CLIText.get().cannotCreateOutputStream, e);
  149.                 }
  150.             }
  151.         }
  152.         return UTF_8;
  153.     }

  154.     /**
  155.      * Initialize the command to work with a repository.
  156.      *
  157.      * @param repository
  158.      *            the opened repository that the command should work on.
  159.      * @param gitDir
  160.      *            value of the {@code --git-dir} command line option, if
  161.      *            {@code repository} is null.
  162.      */
  163.     protected void init(Repository repository, String gitDir) {
  164.         Charset charset = getLogOutputEncodingCharset(repository);

  165.         if (ins == null)
  166.             ins = new FileInputStream(FileDescriptor.in);
  167.         if (outs == null)
  168.             outs = new FileOutputStream(FileDescriptor.out);
  169.         if (errs == null)
  170.             errs = new FileOutputStream(FileDescriptor.err);
  171.         outw = new ThrowingPrintWriter(new BufferedWriter(
  172.                 new OutputStreamWriter(outs, charset)));
  173.         errw = new ThrowingPrintWriter(new BufferedWriter(
  174.                 new OutputStreamWriter(errs, charset)));

  175.         if (repository != null && repository.getDirectory() != null) {
  176.             db = repository;
  177.             gitdir = repository.getDirectory().getAbsolutePath();
  178.         } else {
  179.             db = repository;
  180.             gitdir = gitDir;
  181.         }
  182.     }

  183.     /**
  184.      * Parse arguments and run this command.
  185.      *
  186.      * @param args
  187.      *            command line arguments passed after the command name.
  188.      * @throws java.lang.Exception
  189.      *             an error occurred while processing the command. The main
  190.      *             framework will catch the exception and print a message on
  191.      *             standard error.
  192.      */
  193.     public final void execute(String[] args) throws Exception {
  194.         parseArguments(args);
  195.         switch (sshDriver) {
  196.         case APACHE: {
  197.             SshdSessionFactory factory = new SshdSessionFactory(
  198.                     new JGitKeyCache(), new DefaultProxyDataFactory());
  199.             Runtime.getRuntime()
  200.                     .addShutdownHook(new Thread(factory::close));
  201.             SshSessionFactory.setInstance(factory);
  202.             break;
  203.         }
  204.         case JSCH:
  205.             JschConfigSessionFactory factory = new JschConfigSessionFactory();
  206.             SshSessionFactory.setInstance(factory);
  207.             break;
  208.         default:
  209.             SshSessionFactory.setInstance(null);
  210.             break;
  211.         }
  212.         run();
  213.     }

  214.     /**
  215.      * Parses the command line arguments prior to running.
  216.      * <p>
  217.      * This method should only be invoked by {@link #execute(String[])}, prior
  218.      * to calling {@link #run()}. The default implementation parses all
  219.      * arguments into this object's instance fields.
  220.      *
  221.      * @param args
  222.      *            the arguments supplied on the command line, if any.
  223.      * @throws java.io.IOException
  224.      */
  225.     protected void parseArguments(String[] args) throws IOException {
  226.         final CmdLineParser clp = new CmdLineParser(this);
  227.         help = containsHelp(args);
  228.         try {
  229.             clp.parseArgument(args);
  230.         } catch (CmdLineException err) {
  231.             this.errw.println(CLIText.fatalError(err.getMessage()));
  232.             if (help) {
  233.                 printUsage("", clp); //$NON-NLS-1$
  234.             }
  235.             throw die(true, err);
  236.         }

  237.         if (help) {
  238.             printUsage("", clp); //$NON-NLS-1$
  239.             throw new TerminatedByHelpException();
  240.         }

  241.         argWalk = clp.getRevWalkGently();
  242.     }

  243.     /**
  244.      * Print the usage line
  245.      *
  246.      * @param clp
  247.      *            a {@link org.eclipse.jgit.pgm.opt.CmdLineParser} object.
  248.      * @throws java.io.IOException
  249.      */
  250.     public void printUsageAndExit(CmdLineParser clp) throws IOException {
  251.         printUsageAndExit("", clp); //$NON-NLS-1$
  252.     }

  253.     /**
  254.      * Print an error message and the usage line
  255.      *
  256.      * @param message
  257.      *            a {@link java.lang.String} object.
  258.      * @param clp
  259.      *            a {@link org.eclipse.jgit.pgm.opt.CmdLineParser} object.
  260.      * @throws java.io.IOException
  261.      */
  262.     public void printUsageAndExit(String message, CmdLineParser clp) throws IOException {
  263.         printUsage(message, clp);
  264.         throw die(true);
  265.     }

  266.     /**
  267.      * Print usage help text.
  268.      *
  269.      * @param message
  270.      *            non null
  271.      * @param clp
  272.      *            parser used to print options
  273.      * @throws java.io.IOException
  274.      * @since 4.2
  275.      */
  276.     protected void printUsage(String message, CmdLineParser clp)
  277.             throws IOException {
  278.         errw.println(message);
  279.         errw.print("jgit "); //$NON-NLS-1$
  280.         errw.print(commandName);
  281.         clp.printSingleLineUsage(errw, getResourceBundle());
  282.         errw.println();

  283.         errw.println();
  284.         clp.printUsage(errw, getResourceBundle());
  285.         errw.println();

  286.         errw.flush();
  287.     }

  288.     /**
  289.      * Get error writer
  290.      *
  291.      * @return error writer, typically this is standard error.
  292.      * @since 4.2
  293.      */
  294.     public ThrowingPrintWriter getErrorWriter() {
  295.         return errw;
  296.     }

  297.     /**
  298.      * Get output writer
  299.      *
  300.      * @return output writer, typically this is standard output.
  301.      * @since 4.9
  302.      */
  303.     public ThrowingPrintWriter getOutputWriter() {
  304.         return outw;
  305.     }

  306.     /**
  307.      * Get resource bundle with localized texts
  308.      *
  309.      * @return the resource bundle that will be passed to args4j for purpose of
  310.      *         string localization
  311.      */
  312.     protected ResourceBundle getResourceBundle() {
  313.         return CLIText.get().resourceBundle();
  314.     }

  315.     /**
  316.      * Perform the actions of this command.
  317.      * <p>
  318.      * This method should only be invoked by {@link #execute(String[])}.
  319.      *
  320.      * @throws java.lang.Exception
  321.      *             an error occurred while processing the command. The main
  322.      *             framework will catch the exception and print a message on
  323.      *             standard error.
  324.      */
  325.     protected abstract void run() throws Exception;

  326.     /**
  327.      * Get the repository
  328.      *
  329.      * @return the repository this command accesses.
  330.      */
  331.     public Repository getRepository() {
  332.         return db;
  333.     }

  334.     ObjectId resolve(String s) throws IOException {
  335.         final ObjectId r = db.resolve(s);
  336.         if (r == null)
  337.             throw die(MessageFormat.format(CLIText.get().notARevision, s));
  338.         return r;
  339.     }

  340.     /**
  341.      * Exit the command with an error message
  342.      *
  343.      * @param why
  344.      *            textual explanation
  345.      * @return a runtime exception the caller is expected to throw
  346.      */
  347.     protected static Die die(String why) {
  348.         return new Die(why);
  349.     }

  350.     /**
  351.      * Exit the command with an error message and an exception
  352.      *
  353.      * @param why
  354.      *            textual explanation
  355.      * @param cause
  356.      *            why the command has failed.
  357.      * @return a runtime exception the caller is expected to throw
  358.      */
  359.     protected static Die die(String why, Throwable cause) {
  360.         return new Die(why, cause);
  361.     }

  362.     /**
  363.      * Exit the command
  364.      *
  365.      * @param aborted
  366.      *            boolean indicating that the execution has been aborted before
  367.      *            running
  368.      * @return a runtime exception the caller is expected to throw
  369.      * @since 3.4
  370.      */
  371.     protected static Die die(boolean aborted) {
  372.         return new Die(aborted);
  373.     }

  374.     /**
  375.      * Exit the command
  376.      *
  377.      * @param aborted
  378.      *            boolean indicating that the execution has been aborted before
  379.      *            running
  380.      * @param cause
  381.      *            why the command has failed.
  382.      * @return a runtime exception the caller is expected to throw
  383.      * @since 4.2
  384.      */
  385.     protected static Die die(boolean aborted, Throwable cause) {
  386.         return new Die(aborted, cause);
  387.     }

  388.     String abbreviateRef(String dst, boolean abbreviateRemote) {
  389.         if (dst.startsWith(R_HEADS))
  390.             dst = dst.substring(R_HEADS.length());
  391.         else if (dst.startsWith(R_TAGS))
  392.             dst = dst.substring(R_TAGS.length());
  393.         else if (abbreviateRemote && dst.startsWith(R_REMOTES))
  394.             dst = dst.substring(R_REMOTES.length());
  395.         return dst;
  396.     }

  397.     /**
  398.      * Check if the arguments contain a help option
  399.      *
  400.      * @param args
  401.      *            non null
  402.      * @return true if the given array contains help option
  403.      * @since 4.2
  404.      */
  405.     public static boolean containsHelp(String[] args) {
  406.         for (String str : args) {
  407.             if (str.equals("-h") || str.equals("--help")) { //$NON-NLS-1$ //$NON-NLS-2$
  408.                 return true;
  409.             }
  410.         }
  411.         return false;
  412.     }

  413.     /**
  414.      * Exception thrown by {@link TextBuiltin} if it proceeds 'help' option
  415.      *
  416.      * @since 4.2
  417.      */
  418.     public static class TerminatedByHelpException extends Die {
  419.         private static final long serialVersionUID = 1L;

  420.         /**
  421.          * Default constructor
  422.          */
  423.         public TerminatedByHelpException() {
  424.             super(true);
  425.         }

  426.     }
  427. }