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