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