View Javadoc
1   /*
2    * Copyright (C) 2006, 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  
49  import java.io.File;
50  import java.io.IOException;
51  import java.io.OutputStreamWriter;
52  import java.io.PrintWriter;
53  import java.lang.reflect.InvocationTargetException;
54  import java.net.MalformedURLException;
55  import java.net.URL;
56  import java.text.MessageFormat;
57  import java.util.ArrayList;
58  import java.util.List;
59  import java.util.Locale;
60  import java.util.concurrent.ExecutorService;
61  import java.util.concurrent.Executors;
62  import java.util.concurrent.ThreadFactory;
63  import java.util.concurrent.TimeUnit;
64  
65  import org.eclipse.jgit.awtui.AwtAuthenticator;
66  import org.eclipse.jgit.awtui.AwtCredentialsProvider;
67  import org.eclipse.jgit.errors.TransportException;
68  import org.eclipse.jgit.lfs.BuiltinLFS;
69  import org.eclipse.jgit.lib.Repository;
70  import org.eclipse.jgit.lib.RepositoryBuilder;
71  import org.eclipse.jgit.pgm.internal.CLIText;
72  import org.eclipse.jgit.pgm.opt.CmdLineParser;
73  import org.eclipse.jgit.pgm.opt.SubcommandHandler;
74  import org.eclipse.jgit.transport.HttpTransport;
75  import org.eclipse.jgit.transport.http.apache.HttpClientConnectionFactory;
76  import org.eclipse.jgit.util.CachedAuthenticator;
77  import org.kohsuke.args4j.Argument;
78  import org.kohsuke.args4j.CmdLineException;
79  import org.kohsuke.args4j.Option;
80  import org.kohsuke.args4j.OptionHandlerFilter;
81  
82  /**
83   * Command line entry point.
84   */
85  public class Main {
86  	@Option(name = "--help", usage = "usage_displayThisHelpText", aliases = { "-h" })
87  	private boolean help;
88  
89  	@Option(name = "--version", usage = "usage_displayVersion")
90  	private boolean version;
91  
92  	@Option(name = "--show-stack-trace", usage = "usage_displayThejavaStackTraceOnExceptions")
93  	private boolean showStackTrace;
94  
95  	@Option(name = "--git-dir", metaVar = "metaVar_gitDir", usage = "usage_setTheGitRepositoryToOperateOn")
96  	private String gitdir;
97  
98  	@Argument(index = 0, metaVar = "metaVar_command", required = true, handler = SubcommandHandler.class)
99  	private TextBuiltin subcommand;
100 
101 	@Argument(index = 1, metaVar = "metaVar_arg")
102 	private List<String> arguments = new ArrayList<>();
103 
104 	PrintWriter writer;
105 
106 	private ExecutorService gcExecutor;
107 
108 	/**
109 	 * <p>Constructor for Main.</p>
110 	 */
111 	public Main() {
112 		HttpTransport.setConnectionFactory(new HttpClientConnectionFactory());
113 		BuiltinLFS.register();
114 		gcExecutor = Executors.newSingleThreadExecutor(new ThreadFactory() {
115 			private final ThreadFactory baseFactory = Executors
116 					.defaultThreadFactory();
117 
118 			@Override
119 			public Thread newThread(Runnable taskBody) {
120 				Thread thr = baseFactory.newThread(taskBody);
121 				thr.setName("JGit-autoGc"); //$NON-NLS-1$
122 				return thr;
123 			}
124 		});
125 	}
126 
127 	/**
128 	 * Execute the command line.
129 	 *
130 	 * @param argv
131 	 *            arguments.
132 	 * @throws java.lang.Exception
133 	 */
134 	public static void main(String[] argv) throws Exception {
135 		// make sure built-in filters are registered
136 		BuiltinLFS.register();
137 
138 		new Main().run(argv);
139 	}
140 
141 	/**
142 	 * Parse the command line and execute the requested action.
143 	 *
144 	 * Subclasses should allocate themselves and then invoke this method:
145 	 *
146 	 * <pre>
147 	 * class ExtMain {
148 	 * 	public static void main(String[] argv) {
149 	 * 		new ExtMain().run(argv);
150 	 * 	}
151 	 * }
152 	 * </pre>
153 	 *
154 	 * @param argv
155 	 *            arguments.
156 	 * @throws java.lang.Exception
157 	 */
158 	protected void run(String[] argv) throws Exception {
159 		writer = createErrorWriter();
160 		try {
161 			if (!installConsole()) {
162 				AwtAuthenticator.install();
163 				AwtCredentialsProvider.install();
164 			}
165 			configureHttpProxy();
166 			execute(argv);
167 		} catch (Die err) {
168 			if (err.isAborted()) {
169 				exit(1, err);
170 			}
171 			writer.println(CLIText.fatalError(err.getMessage()));
172 			if (showStackTrace) {
173 				err.printStackTrace(writer);
174 			}
175 			exit(128, err);
176 		} catch (Exception err) {
177 			// Try to detect errno == EPIPE and exit normally if that happens
178 			// There may be issues with operating system versions and locale,
179 			// but we can probably assume that these messages will not be thrown
180 			// under other circumstances.
181 			if (err.getClass() == IOException.class) {
182 				// Linux, OS X
183 				if (err.getMessage().equals("Broken pipe")) { //$NON-NLS-1$
184 					exit(0, err);
185 				}
186 				// Windows
187 				if (err.getMessage().equals("The pipe is being closed")) { //$NON-NLS-1$
188 					exit(0, err);
189 				}
190 			}
191 			if (!showStackTrace && err.getCause() != null
192 					&& err instanceof TransportException) {
193 				writer.println(CLIText.fatalError(err.getCause().getMessage()));
194 			}
195 
196 			if (err.getClass().getName().startsWith("org.eclipse.jgit.errors.")) { //$NON-NLS-1$
197 				writer.println(CLIText.fatalError(err.getMessage()));
198 				if (showStackTrace) {
199 					err.printStackTrace();
200 				}
201 				exit(128, err);
202 			}
203 			err.printStackTrace();
204 			exit(1, err);
205 		}
206 		if (System.out.checkError()) {
207 			writer.println(CLIText.get().unknownIoErrorStdout);
208 			exit(1, null);
209 		}
210 		if (writer.checkError()) {
211 			// No idea how to present an error here, most likely disk full or
212 			// broken pipe
213 			exit(1, null);
214 		}
215 		gcExecutor.shutdown();
216 		gcExecutor.awaitTermination(10, TimeUnit.MINUTES);
217 	}
218 
219 	PrintWriter createErrorWriter() {
220 		return new PrintWriter(new OutputStreamWriter(System.err, UTF_8));
221 	}
222 
223 	private void execute(String[] argv) throws Exception {
224 		final CmdLineParser clp = new SubcommandLineParser(this);
225 
226 		try {
227 			clp.parseArgument(argv);
228 		} catch (CmdLineException err) {
229 			if (argv.length > 0 && !help && !version) {
230 				writer.println(CLIText.fatalError(err.getMessage()));
231 				writer.flush();
232 				exit(1, err);
233 			}
234 		}
235 
236 		if (argv.length == 0 || help) {
237 			final String ex = clp.printExample(OptionHandlerFilter.ALL,
238 					CLIText.get().resourceBundle());
239 			writer.println("jgit" + ex + " command [ARG ...]"); //$NON-NLS-1$ //$NON-NLS-2$
240 			if (help) {
241 				writer.println();
242 				clp.printUsage(writer, CLIText.get().resourceBundle());
243 				writer.println();
244 			} else if (subcommand == null) {
245 				writer.println();
246 				writer.println(CLIText.get().mostCommonlyUsedCommandsAre);
247 				final CommandRef[] common = CommandCatalog.common();
248 				int width = 0;
249 				for (CommandRef c : common) {
250 					width = Math.max(width, c.getName().length());
251 				}
252 				width += 2;
253 
254 				for (CommandRef c : common) {
255 					writer.print(' ');
256 					writer.print(c.getName());
257 					for (int i = c.getName().length(); i < width; i++) {
258 						writer.print(' ');
259 					}
260 					writer.print(CLIText.get().resourceBundle().getString(c.getUsage()));
261 					writer.println();
262 				}
263 				writer.println();
264 			}
265 			writer.flush();
266 			exit(1, null);
267 		}
268 
269 		if (version) {
270 			String cmdId = Version.class.getSimpleName()
271 					.toLowerCase(Locale.ROOT);
272 			subcommand = CommandCatalog.get(cmdId).create();
273 		}
274 
275 		final TextBuiltin cmd = subcommand;
276 		init(cmd);
277 		try {
278 			cmd.execute(arguments.toArray(new String[0]));
279 		} finally {
280 			if (cmd.outw != null) {
281 				cmd.outw.flush();
282 			}
283 			if (cmd.errw != null) {
284 				cmd.errw.flush();
285 			}
286 		}
287 	}
288 
289 	void init(TextBuiltin cmd) throws IOException {
290 		if (cmd.requiresRepository()) {
291 			cmd.init(openGitDir(gitdir), null);
292 		} else {
293 			cmd.init(null, gitdir);
294 		}
295 	}
296 
297 	/**
298 	 * @param status
299 	 * @param t
300 	 *            can be {@code null}
301 	 * @throws Exception
302 	 */
303 	void exit(int status, Exception t) throws Exception {
304 		writer.flush();
305 		System.exit(status);
306 	}
307 
308 	/**
309 	 * Evaluate the {@code --git-dir} option and open the repository.
310 	 *
311 	 * @param aGitdir
312 	 *            the {@code --git-dir} option given on the command line. May be
313 	 *            null if it was not supplied.
314 	 * @return the repository to operate on.
315 	 * @throws java.io.IOException
316 	 *             the repository cannot be opened.
317 	 */
318 	protected Repository openGitDir(String aGitdir) throws IOException {
319 		RepositoryBuilder rb = new RepositoryBuilder() //
320 				.setGitDir(aGitdir != null ? new File(aGitdir) : null) //
321 				.readEnvironment() //
322 				.findGitDir();
323 		if (rb.getGitDir() == null)
324 			throw new Die(CLIText.get().cantFindGitDirectory);
325 		return rb.build();
326 	}
327 
328 	private static boolean installConsole() {
329 		try {
330 			install("org.eclipse.jgit.console.ConsoleAuthenticator"); //$NON-NLS-1$
331 			install("org.eclipse.jgit.console.ConsoleCredentialsProvider"); //$NON-NLS-1$
332 			return true;
333 		} catch (ClassNotFoundException e) {
334 			return false;
335 		} catch (NoClassDefFoundError e) {
336 			return false;
337 		} catch (UnsupportedClassVersionError e) {
338 			return false;
339 
340 		} catch (IllegalArgumentException e) {
341 			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
342 		} catch (SecurityException e) {
343 			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
344 		} catch (IllegalAccessException e) {
345 			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
346 		} catch (InvocationTargetException e) {
347 			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
348 		} catch (NoSuchMethodException e) {
349 			throw new RuntimeException(CLIText.get().cannotSetupConsole, e);
350 		}
351 	}
352 
353 	private static void install(String name)
354 			throws IllegalAccessException, InvocationTargetException,
355 			NoSuchMethodException, ClassNotFoundException {
356 		try {
357 			Class.forName(name).getMethod("install").invoke(null); //$NON-NLS-1$
358 		} catch (InvocationTargetException e) {
359 			if (e.getCause() instanceof RuntimeException)
360 				throw (RuntimeException) e.getCause();
361 			if (e.getCause() instanceof Error)
362 				throw (Error) e.getCause();
363 			throw e;
364 		}
365 	}
366 
367 	/**
368 	 * Configure the JRE's standard HTTP based on <code>http_proxy</code>.
369 	 * <p>
370 	 * The popular libcurl library honors the <code>http_proxy</code>,
371 	 * <code>https_proxy</code> environment variables as a means of specifying
372 	 * an HTTP/S proxy for requests made behind a firewall. This is not natively
373 	 * recognized by the JRE, so this method can be used by command line
374 	 * utilities to configure the JRE before the first request is sent. The
375 	 * information found in the environment variables is copied to the
376 	 * associated system properties. This is not done when the system properties
377 	 * are already set. The default way of telling java programs about proxies
378 	 * (the system properties) takes precedence over environment variables.
379 	 *
380 	 * @throws MalformedURLException
381 	 *             the value in <code>http_proxy</code> or
382 	 *             <code>https_proxy</code> is unsupportable.
383 	 */
384 	static void configureHttpProxy() throws MalformedURLException {
385 		for (String protocol : new String[] { "http", "https" }) { //$NON-NLS-1$ //$NON-NLS-2$
386 			if (System.getProperty(protocol + ".proxyHost") != null) { //$NON-NLS-1$
387 				continue;
388 			}
389 			String s = System.getenv(protocol + "_proxy"); //$NON-NLS-1$
390 			if (s == null && protocol.equals("https")) { //$NON-NLS-1$
391 				s = System.getenv("HTTPS_PROXY"); //$NON-NLS-1$
392 			}
393 			if (s == null || s.equals("")) { //$NON-NLS-1$
394 				continue;
395 			}
396 
397 			final URL u = new URL(
398 					(s.indexOf("://") == -1) ? protocol + "://" + s : s); //$NON-NLS-1$ //$NON-NLS-2$
399 			if (!u.getProtocol().startsWith("http")) //$NON-NLS-1$
400 				throw new MalformedURLException(MessageFormat.format(
401 						CLIText.get().invalidHttpProxyOnlyHttpSupported, s));
402 
403 			final String proxyHost = u.getHost();
404 			final int proxyPort = u.getPort();
405 
406 			System.setProperty(protocol + ".proxyHost", proxyHost); //$NON-NLS-1$
407 			if (proxyPort > 0)
408 				System.setProperty(protocol + ".proxyPort", //$NON-NLS-1$
409 						String.valueOf(proxyPort));
410 
411 			final String userpass = u.getUserInfo();
412 			if (userpass != null && userpass.contains(":")) { //$NON-NLS-1$
413 				final int c = userpass.indexOf(':');
414 				final String user = userpass.substring(0, c);
415 				final String pass = userpass.substring(c + 1);
416 				CachedAuthenticator.add(
417 						new CachedAuthenticator.CachedAuthentication(proxyHost,
418 								proxyPort, user, pass));
419 			}
420 		}
421 	}
422 
423 	/**
424 	 * Parser for subcommands which doesn't stop parsing on help options and so
425 	 * proceeds all specified options
426 	 */
427 	static class SubcommandLineParser extends CmdLineParser {
428 		public SubcommandLineParser(Object bean) {
429 			super(bean);
430 		}
431 
432 		@Override
433 		protected boolean containsHelp(String... args) {
434 			return false;
435 		}
436 	}
437 }