View Javadoc
1   /*
2    * Copyright (C) 2007, Robin Rosenberg <robin.rosenberg@dewire.com>
3    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
4    * and other copyright owners as documented in the project's IP log.
5    *
6    * This program and the accompanying materials are made available
7    * under the terms of the Eclipse Distribution License v1.0 which
8    * accompanies this distribution, is reproduced below, and is
9    * available at http://www.eclipse.org/org/documents/edl-v10.php
10   *
11   * All rights reserved.
12   *
13   * Redistribution and use in source and binary forms, with or
14   * without modification, are permitted provided that the following
15   * conditions are met:
16   *
17   * - Redistributions of source code must retain the above copyright
18   *   notice, this list of conditions and the following disclaimer.
19   *
20   * - Redistributions in binary form must reproduce the above
21   *   copyright notice, this list of conditions and the following
22   *   disclaimer in the documentation and/or other materials provided
23   *   with the distribution.
24   *
25   * - Neither the name of the Eclipse Foundation, Inc. nor the
26   *   names of its contributors may be used to endorse or promote
27   *   products derived from this software without specific prior
28   *   written permission.
29   *
30   * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
31   * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
32   * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
33   * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
34   * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
35   * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
36   * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
37   * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
38   * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
39   * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
40   * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
41   * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
42   * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
43   */
44  
45  package org.eclipse.jgit.pgm;
46  
47  import static java.nio.charset.StandardCharsets.UTF_8;
48  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_LOG_OUTPUT_ENCODING;
49  import static org.eclipse.jgit.lib.ConfigConstants.CONFIG_SECTION_I18N;
50  import static org.eclipse.jgit.lib.Constants.R_HEADS;
51  import static org.eclipse.jgit.lib.Constants.R_REMOTES;
52  import static org.eclipse.jgit.lib.Constants.R_TAGS;
53  
54  import java.io.BufferedWriter;
55  import java.io.FileDescriptor;
56  import java.io.FileInputStream;
57  import java.io.FileOutputStream;
58  import java.io.IOException;
59  import java.io.InputStream;
60  import java.io.OutputStream;
61  import java.io.OutputStreamWriter;
62  import java.nio.charset.Charset;
63  import java.text.MessageFormat;
64  import java.util.ResourceBundle;
65  
66  import org.eclipse.jgit.lib.ObjectId;
67  import org.eclipse.jgit.lib.Repository;
68  import org.eclipse.jgit.pgm.internal.CLIText;
69  import org.eclipse.jgit.pgm.internal.SshDriver;
70  import org.eclipse.jgit.pgm.opt.CmdLineParser;
71  import org.eclipse.jgit.revwalk.RevWalk;
72  import org.eclipse.jgit.transport.SshSessionFactory;
73  import org.eclipse.jgit.transport.sshd.DefaultProxyDataFactory;
74  import org.eclipse.jgit.transport.sshd.JGitKeyCache;
75  import org.eclipse.jgit.transport.sshd.SshdSessionFactory;
76  import org.eclipse.jgit.util.io.ThrowingPrintWriter;
77  import org.kohsuke.args4j.CmdLineException;
78  import org.kohsuke.args4j.Option;
79  
80  /**
81   * Abstract command which can be invoked from the command line.
82   * <p>
83   * Commands are configured with a single "current" repository and then the
84   * {@link #execute(String[])} method is invoked with the arguments that appear
85   * on the command line after the command name.
86   * <p>
87   * Command constructors should perform as little work as possible as they may be
88   * invoked very early during process loading, and the command may not execute
89   * even though it was constructed.
90   */
91  public abstract class TextBuiltin {
92  	private String commandName;
93  
94  	@Option(name = "--help", usage = "usage_displayThisHelpText", aliases = { "-h" })
95  	private boolean help;
96  
97  	@Option(name = "--ssh", usage = "usage_sshDriver")
98  	private SshDriver sshDriver = SshDriver.JSCH;
99  
100 	/**
101 	 * Input stream, typically this is standard input.
102 	 *
103 	 * @since 3.4
104 	 */
105 	protected InputStream ins;
106 
107 	/**
108 	 * Writer to output to, typically this is standard output.
109 	 *
110 	 * @since 2.2
111 	 */
112 	protected ThrowingPrintWriter outw;
113 
114 	/**
115 	 * Stream to output to, typically this is standard output.
116 	 *
117 	 * @since 2.2
118 	 */
119 	protected OutputStream outs;
120 
121 	/**
122 	 * Error writer, typically this is standard error.
123 	 *
124 	 * @since 3.4
125 	 */
126 	protected ThrowingPrintWriter errw;
127 
128 	/**
129 	 * Error output stream, typically this is standard error.
130 	 *
131 	 * @since 3.4
132 	 */
133 	protected OutputStream errs;
134 
135 	/** Git repository the command was invoked within. */
136 	protected Repository db;
137 
138 	/** Directory supplied via --git-dir command line option. */
139 	protected String gitdir;
140 
141 	/** RevWalk used during command line parsing, if it was required. */
142 	protected RevWalk argWalk;
143 
144 	final void setCommandName(String name) {
145 		commandName = name;
146 	}
147 
148 	/**
149 	 * If this command requires a repository.
150 	 *
151 	 * @return true if {@link #db}/{@link #getRepository()} is required
152 	 */
153 	protected boolean requiresRepository() {
154 		return true;
155 	}
156 
157 	/**
158 	 * Initializes the command to work with a repository, including setting the
159 	 * output and error streams.
160 	 *
161 	 * @param repository
162 	 *            the opened repository that the command should work on.
163 	 * @param gitDir
164 	 *            value of the {@code --git-dir} command line option, if
165 	 *            {@code repository} is null.
166 	 * @param input
167 	 *            input stream from which input will be read
168 	 * @param output
169 	 *            output stream to which output will be written
170 	 * @param error
171 	 *            error stream to which errors will be written
172 	 * @since 4.9
173 	 */
174 	public void initRaw(final Repository repository, final String gitDir,
175 			InputStream input, OutputStream output, OutputStream error) {
176 		this.ins = input;
177 		this.outs = output;
178 		this.errs = error;
179 		init(repository, gitDir);
180 	}
181 
182 	/**
183 	 * Get the log output encoding specified in the repository's
184 	 * {@code i18n.logOutputEncoding} configuration.
185 	 *
186 	 * @param repository
187 	 *            the repository.
188 	 * @return Charset corresponding to {@code i18n.logOutputEncoding}, or
189 	 *         {@code UTF_8}.
190 	 */
191 	private Charset getLogOutputEncodingCharset(Repository repository) {
192 		if (repository != null) {
193 			String logOutputEncoding = repository.getConfig().getString(
194 					CONFIG_SECTION_I18N, null, CONFIG_KEY_LOG_OUTPUT_ENCODING);
195 			if (logOutputEncoding != null) {
196 				try {
197 					return Charset.forName(logOutputEncoding);
198 				} catch (IllegalArgumentException e) {
199 					throw die(CLIText.get().cannotCreateOutputStream, e);
200 				}
201 			}
202 		}
203 		return UTF_8;
204 	}
205 
206 	/**
207 	 * Initialize the command to work with a repository.
208 	 *
209 	 * @param repository
210 	 *            the opened repository that the command should work on.
211 	 * @param gitDir
212 	 *            value of the {@code --git-dir} command line option, if
213 	 *            {@code repository} is null.
214 	 */
215 	protected void init(Repository repository, String gitDir) {
216 		Charset charset = getLogOutputEncodingCharset(repository);
217 
218 		if (ins == null)
219 			ins = new FileInputStream(FileDescriptor.in);
220 		if (outs == null)
221 			outs = new FileOutputStream(FileDescriptor.out);
222 		if (errs == null)
223 			errs = new FileOutputStream(FileDescriptor.err);
224 		outw = new ThrowingPrintWriter(new BufferedWriter(
225 				new OutputStreamWriter(outs, charset)));
226 		errw = new ThrowingPrintWriter(new BufferedWriter(
227 				new OutputStreamWriter(errs, charset)));
228 
229 		if (repository != null && repository.getDirectory() != null) {
230 			db = repository;
231 			gitdir = repository.getDirectory().getAbsolutePath();
232 		} else {
233 			db = repository;
234 			gitdir = gitDir;
235 		}
236 	}
237 
238 	/**
239 	 * Parse arguments and run this command.
240 	 *
241 	 * @param args
242 	 *            command line arguments passed after the command name.
243 	 * @throws java.lang.Exception
244 	 *             an error occurred while processing the command. The main
245 	 *             framework will catch the exception and print a message on
246 	 *             standard error.
247 	 */
248 	public final void execute(String[] args) throws Exception {
249 		parseArguments(args);
250 		switch (sshDriver) {
251 		case APACHE: {
252 			SshdSessionFactory factory = new SshdSessionFactory(
253 					new JGitKeyCache(), new DefaultProxyDataFactory());
254 			Runtime.getRuntime()
255 					.addShutdownHook(new Thread(() -> factory.close()));
256 			SshSessionFactory.setInstance(factory);
257 			break;
258 		}
259 		case JSCH:
260 		default:
261 			SshSessionFactory.setInstance(null);
262 			break;
263 		}
264 		run();
265 	}
266 
267 	/**
268 	 * Parses the command line arguments prior to running.
269 	 * <p>
270 	 * This method should only be invoked by {@link #execute(String[])}, prior
271 	 * to calling {@link #run()}. The default implementation parses all
272 	 * arguments into this object's instance fields.
273 	 *
274 	 * @param args
275 	 *            the arguments supplied on the command line, if any.
276 	 * @throws java.io.IOException
277 	 */
278 	protected void parseArguments(String[] args) throws IOException {
279 		final CmdLineParserParser.html#CmdLineParser">CmdLineParser clp = new CmdLineParser(this);
280 		help = containsHelp(args);
281 		try {
282 			clp.parseArgument(args);
283 		} catch (CmdLineException err) {
284 			this.errw.println(CLIText.fatalError(err.getMessage()));
285 			if (help) {
286 				printUsage("", clp); //$NON-NLS-1$
287 			}
288 			throw die(true, err);
289 		}
290 
291 		if (help) {
292 			printUsage("", clp); //$NON-NLS-1$
293 			throw new TerminatedByHelpException();
294 		}
295 
296 		argWalk = clp.getRevWalkGently();
297 	}
298 
299 	/**
300 	 * Print the usage line
301 	 *
302 	 * @param clp
303 	 *            a {@link org.eclipse.jgit.pgm.opt.CmdLineParser} object.
304 	 * @throws java.io.IOException
305 	 */
306 	public void printUsageAndExit(CmdLineParser clp) throws IOException {
307 		printUsageAndExit("", clp); //$NON-NLS-1$
308 	}
309 
310 	/**
311 	 * Print an error message and the usage line
312 	 *
313 	 * @param message
314 	 *            a {@link java.lang.String} object.
315 	 * @param clp
316 	 *            a {@link org.eclipse.jgit.pgm.opt.CmdLineParser} object.
317 	 * @throws java.io.IOException
318 	 */
319 	public void printUsageAndExit(String message, CmdLineParser clp) throws IOException {
320 		printUsage(message, clp);
321 		throw die(true);
322 	}
323 
324 	/**
325 	 * Print usage help text.
326 	 *
327 	 * @param message
328 	 *            non null
329 	 * @param clp
330 	 *            parser used to print options
331 	 * @throws java.io.IOException
332 	 * @since 4.2
333 	 */
334 	protected void printUsage(String message, CmdLineParser clp)
335 			throws IOException {
336 		errw.println(message);
337 		errw.print("jgit "); //$NON-NLS-1$
338 		errw.print(commandName);
339 		clp.printSingleLineUsage(errw, getResourceBundle());
340 		errw.println();
341 
342 		errw.println();
343 		clp.printUsage(errw, getResourceBundle());
344 		errw.println();
345 
346 		errw.flush();
347 	}
348 
349 	/**
350 	 * Get error writer
351 	 *
352 	 * @return error writer, typically this is standard error.
353 	 * @since 4.2
354 	 */
355 	public ThrowingPrintWriter getErrorWriter() {
356 		return errw;
357 	}
358 
359 	/**
360 	 * Get output writer
361 	 *
362 	 * @return output writer, typically this is standard output.
363 	 * @since 4.9
364 	 */
365 	public ThrowingPrintWriter getOutputWriter() {
366 		return outw;
367 	}
368 
369 	/**
370 	 * Get resource bundle with localized texts
371 	 *
372 	 * @return the resource bundle that will be passed to args4j for purpose of
373 	 *         string localization
374 	 */
375 	protected ResourceBundle getResourceBundle() {
376 		return CLIText.get().resourceBundle();
377 	}
378 
379 	/**
380 	 * Perform the actions of this command.
381 	 * <p>
382 	 * This method should only be invoked by {@link #execute(String[])}.
383 	 *
384 	 * @throws java.lang.Exception
385 	 *             an error occurred while processing the command. The main
386 	 *             framework will catch the exception and print a message on
387 	 *             standard error.
388 	 */
389 	protected abstract void run() throws Exception;
390 
391 	/**
392 	 * Get the repository
393 	 *
394 	 * @return the repository this command accesses.
395 	 */
396 	public Repository getRepository() {
397 		return db;
398 	}
399 
400 	ObjectId resolve(String s) throws IOException {
401 		final ObjectId r = db.resolve(s);
402 		if (r == null)
403 			throw die(MessageFormat.format(CLIText.get().notARevision, s));
404 		return r;
405 	}
406 
407 	/**
408 	 * Exit the command with an error message
409 	 *
410 	 * @param why
411 	 *            textual explanation
412 	 * @return a runtime exception the caller is expected to throw
413 	 */
414 	protected static Die die(String why) {
415 		return new Die(why);
416 	}
417 
418 	/**
419 	 * Exit the command with an error message and an exception
420 	 *
421 	 * @param why
422 	 *            textual explanation
423 	 * @param cause
424 	 *            why the command has failed.
425 	 * @return a runtime exception the caller is expected to throw
426 	 */
427 	protected static Die die(String why, Throwable cause) {
428 		return new Die(why, cause);
429 	}
430 
431 	/**
432 	 * Exit the command
433 	 *
434 	 * @param aborted
435 	 *            boolean indicating that the execution has been aborted before
436 	 *            running
437 	 * @return a runtime exception the caller is expected to throw
438 	 * @since 3.4
439 	 */
440 	protected static Die die(boolean aborted) {
441 		return new Die(aborted);
442 	}
443 
444 	/**
445 	 * Exit the command
446 	 *
447 	 * @param aborted
448 	 *            boolean indicating that the execution has been aborted before
449 	 *            running
450 	 * @param cause
451 	 *            why the command has failed.
452 	 * @return a runtime exception the caller is expected to throw
453 	 * @since 4.2
454 	 */
455 	protected static Die die(boolean aborted, Throwable cause) {
456 		return new Die(aborted, cause);
457 	}
458 
459 	String abbreviateRef(String dst, boolean abbreviateRemote) {
460 		if (dst.startsWith(R_HEADS))
461 			dst = dst.substring(R_HEADS.length());
462 		else if (dst.startsWith(R_TAGS))
463 			dst = dst.substring(R_TAGS.length());
464 		else if (abbreviateRemote && dst.startsWith(R_REMOTES))
465 			dst = dst.substring(R_REMOTES.length());
466 		return dst;
467 	}
468 
469 	/**
470 	 * Check if the arguments contain a help option
471 	 *
472 	 * @param args
473 	 *            non null
474 	 * @return true if the given array contains help option
475 	 * @since 4.2
476 	 */
477 	public static boolean containsHelp(String[] args) {
478 		for (String str : args) {
479 			if (str.equals("-h") || str.equals("--help")) { //$NON-NLS-1$ //$NON-NLS-2$
480 				return true;
481 			}
482 		}
483 		return false;
484 	}
485 
486 	/**
487 	 * Exception thrown by {@link TextBuiltin} if it proceeds 'help' option
488 	 *
489 	 * @since 4.2
490 	 */
491 	public static class TerminatedByHelpException extends Die {
492 		private static final long serialVersionUID = 1L;
493 
494 		/**
495 		 * Default constructor
496 		 */
497 		public TerminatedByHelpException() {
498 			super(true);
499 		}
500 
501 	}
502 }