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 }