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