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