1 /*
2 * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
3 * and other copyright owners as documented in the project's IP log.
4 *
5 * This program and the accompanying materials are made available
6 * under the terms of the Eclipse Distribution License v1.0 which
7 * accompanies this distribution, is reproduced below, and is
8 * available at http://www.eclipse.org/org/documents/edl-v10.php
9 *
10 * All rights reserved.
11 *
12 * Redistribution and use in source and binary forms, with or
13 * without modification, are permitted provided that the following
14 * conditions are met:
15 *
16 * - Redistributions of source code must retain the above copyright
17 * notice, this list of conditions and the following disclaimer.
18 *
19 * - Redistributions in binary form must reproduce the above
20 * copyright notice, this list of conditions and the following
21 * disclaimer in the documentation and/or other materials provided
22 * with the distribution.
23 *
24 * - Neither the name of the Eclipse Foundation, Inc. nor the
25 * names of its contributors may be used to endorse or promote
26 * products derived from this software without specific prior
27 * written permission.
28 *
29 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
30 * CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
31 * INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
32 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
33 * ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
34 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
35 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
36 * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
37 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
38 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
39 * STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
40 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
41 * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
42 */
43
44 package org.eclipse.jgit.util;
45
46 import java.io.BufferedReader;
47 import java.io.BufferedWriter;
48 import java.io.File;
49 import java.io.IOException;
50 import java.io.InputStream;
51 import java.io.InputStreamReader;
52 import java.io.OutputStream;
53 import java.io.OutputStreamWriter;
54 import java.io.PrintStream;
55 import java.io.PrintWriter;
56 import java.nio.charset.Charset;
57 import java.security.AccessController;
58 import java.security.PrivilegedAction;
59 import java.text.MessageFormat;
60 import java.util.Arrays;
61 import java.util.HashMap;
62 import java.util.Map;
63 import java.util.concurrent.Callable;
64 import java.util.concurrent.ExecutorService;
65 import java.util.concurrent.Executors;
66 import java.util.concurrent.TimeUnit;
67 import java.util.concurrent.atomic.AtomicBoolean;
68
69 import org.eclipse.jgit.api.errors.JGitInternalException;
70 import org.eclipse.jgit.internal.JGitText;
71 import org.eclipse.jgit.lib.Constants;
72 import org.eclipse.jgit.lib.Repository;
73 import org.eclipse.jgit.util.ProcessResult.Status;
74 import org.slf4j.Logger;
75 import org.slf4j.LoggerFactory;
76
77 /** Abstraction to support various file system operations not in Java. */
78 public abstract class FS {
79 /**
80 * This class creates FS instances. It will be overridden by a Java7 variant
81 * if such can be detected in {@link #detect(Boolean)}.
82 *
83 * @since 3.0
84 */
85 public static class FSFactory {
86 /**
87 * Constructor
88 */
89 protected FSFactory() {
90 // empty
91 }
92
93 /**
94 * Detect the file system
95 *
96 * @param cygwinUsed
97 * @return FS instance
98 */
99 public FS detect(Boolean cygwinUsed) {
100 if (SystemReader.getInstance().isWindows()) {
101 if (cygwinUsed == null)
102 cygwinUsed = Boolean.valueOf(FS_Win32_Cygwin.isCygwin());
103 if (cygwinUsed.booleanValue())
104 return new FS_Win32_Cygwin();
105 else
106 return new FS_Win32();
107 } else {
108 return new FS_POSIX();
109 }
110 }
111 }
112
113 private final static Logger LOG = LoggerFactory.getLogger(FS.class);
114
115 /** The auto-detected implementation selected for this operating system and JRE. */
116 public static final FS DETECTED = detect();
117
118 private static FSFactory factory;
119
120 /**
121 * Auto-detect the appropriate file system abstraction.
122 *
123 * @return detected file system abstraction
124 */
125 public static FS detect() {
126 return detect(null);
127 }
128
129 /**
130 * Auto-detect the appropriate file system abstraction, taking into account
131 * the presence of a Cygwin installation on the system. Using jgit in
132 * combination with Cygwin requires a more elaborate (and possibly slower)
133 * resolution of file system paths.
134 *
135 * @param cygwinUsed
136 * <ul>
137 * <li><code>Boolean.TRUE</code> to assume that Cygwin is used in
138 * combination with jgit</li>
139 * <li><code>Boolean.FALSE</code> to assume that Cygwin is
140 * <b>not</b> used with jgit</li>
141 * <li><code>null</code> to auto-detect whether a Cygwin
142 * installation is present on the system and in this case assume
143 * that Cygwin is used</li>
144 * </ul>
145 *
146 * Note: this parameter is only relevant on Windows.
147 *
148 * @return detected file system abstraction
149 */
150 public static FS detect(Boolean cygwinUsed) {
151 if (factory == null) {
152 factory = new FS.FSFactory();
153 }
154 return factory.detect(cygwinUsed);
155 }
156
157 private volatile Holder<File> userHome;
158
159 private volatile Holder<File> gitSystemConfig;
160
161 /**
162 * Constructs a file system abstraction.
163 */
164 protected FS() {
165 // Do nothing by default.
166 }
167
168 /**
169 * Initialize this FS using another's current settings.
170 *
171 * @param src
172 * the source FS to copy from.
173 */
174 protected FS(FS src) {
175 userHome = src.userHome;
176 gitSystemConfig = src.gitSystemConfig;
177 }
178
179 /** @return a new instance of the same type of FS. */
180 public abstract FS newInstance();
181
182 /**
183 * Does this operating system and JRE support the execute flag on files?
184 *
185 * @return true if this implementation can provide reasonably accurate
186 * executable bit information; false otherwise.
187 */
188 public abstract boolean supportsExecute();
189
190 /**
191 * Does this operating system and JRE supports symbolic links. The
192 * capability to handle symbolic links is detected at runtime.
193 *
194 * @return true if symbolic links may be used
195 * @since 3.0
196 */
197 public boolean supportsSymlinks() {
198 return false;
199 }
200
201 /**
202 * Is this file system case sensitive
203 *
204 * @return true if this implementation is case sensitive
205 */
206 public abstract boolean isCaseSensitive();
207
208 /**
209 * Determine if the file is executable (or not).
210 * <p>
211 * Not all platforms and JREs support executable flags on files. If the
212 * feature is unsupported this method will always return false.
213 * <p>
214 * <em>If the platform supports symbolic links and <code>f</code> is a symbolic link
215 * this method returns false, rather than the state of the executable flags
216 * on the target file.</em>
217 *
218 * @param f
219 * abstract path to test.
220 * @return true if the file is believed to be executable by the user.
221 */
222 public abstract boolean canExecute(File f);
223
224 /**
225 * Set a file to be executable by the user.
226 * <p>
227 * Not all platforms and JREs support executable flags on files. If the
228 * feature is unsupported this method will always return false and no
229 * changes will be made to the file specified.
230 *
231 * @param f
232 * path to modify the executable status of.
233 * @param canExec
234 * true to enable execution; false to disable it.
235 * @return true if the change succeeded; false otherwise.
236 */
237 public abstract boolean setExecute(File f, boolean canExec);
238
239 /**
240 * Get the last modified time of a file system object. If the OS/JRE support
241 * symbolic links, the modification time of the link is returned, rather
242 * than that of the link target.
243 *
244 * @param f
245 * @return last modified time of f
246 * @throws IOException
247 * @since 3.0
248 */
249 public long lastModified(File f) throws IOException {
250 return FileUtils.lastModified(f);
251 }
252
253 /**
254 * Set the last modified time of a file system object. If the OS/JRE support
255 * symbolic links, the link is modified, not the target,
256 *
257 * @param f
258 * @param time
259 * @throws IOException
260 * @since 3.0
261 */
262 public void setLastModified(File f, long time) throws IOException {
263 FileUtils.setLastModified(f, time);
264 }
265
266 /**
267 * Get the length of a file or link, If the OS/JRE supports symbolic links
268 * it's the length of the link, else the length of the target.
269 *
270 * @param path
271 * @return length of a file
272 * @throws IOException
273 * @since 3.0
274 */
275 public long length(File path) throws IOException {
276 return FileUtils.getLength(path);
277 }
278
279 /**
280 * Delete a file. Throws an exception if delete fails.
281 *
282 * @param f
283 * @throws IOException
284 * this may be a Java7 subclass with detailed information
285 * @since 3.3
286 */
287 public void delete(File f) throws IOException {
288 FileUtils.delete(f);
289 }
290
291 /**
292 * Resolve this file to its actual path name that the JRE can use.
293 * <p>
294 * This method can be relatively expensive. Computing a translation may
295 * require forking an external process per path name translated. Callers
296 * should try to minimize the number of translations necessary by caching
297 * the results.
298 * <p>
299 * Not all platforms and JREs require path name translation. Currently only
300 * Cygwin on Win32 require translation for Cygwin based paths.
301 *
302 * @param dir
303 * directory relative to which the path name is.
304 * @param name
305 * path name to translate.
306 * @return the translated path. <code>new File(dir,name)</code> if this
307 * platform does not require path name translation.
308 */
309 public File resolve(final File dir, final String name) {
310 final File abspn = new File(name);
311 if (abspn.isAbsolute())
312 return abspn;
313 return new File(dir, name);
314 }
315
316 /**
317 * Determine the user's home directory (location where preferences are).
318 * <p>
319 * This method can be expensive on the first invocation if path name
320 * translation is required. Subsequent invocations return a cached result.
321 * <p>
322 * Not all platforms and JREs require path name translation. Currently only
323 * Cygwin on Win32 requires translation of the Cygwin HOME directory.
324 *
325 * @return the user's home directory; null if the user does not have one.
326 */
327 public File userHome() {
328 Holder<File> p = userHome;
329 if (p == null) {
330 p = new Holder<File>(userHomeImpl());
331 userHome = p;
332 }
333 return p.value;
334 }
335
336 /**
337 * Set the user's home directory location.
338 *
339 * @param path
340 * the location of the user's preferences; null if there is no
341 * home directory for the current user.
342 * @return {@code this}.
343 */
344 public FS setUserHome(File path) {
345 userHome = new Holder<File>(path);
346 return this;
347 }
348
349 /**
350 * Does this file system have problems with atomic renames?
351 *
352 * @return true if the caller should retry a failed rename of a lock file.
353 */
354 public abstract boolean retryFailedLockFileCommit();
355
356 /**
357 * Determine the user's home directory (location where preferences are).
358 *
359 * @return the user's home directory; null if the user does not have one.
360 */
361 protected File userHomeImpl() {
362 final String home = AccessController
363 .doPrivileged(new PrivilegedAction<String>() {
364 public String run() {
365 return System.getProperty("user.home"); //$NON-NLS-1$
366 }
367 });
368 if (home == null || home.length() == 0)
369 return null;
370 return new File(home).getAbsoluteFile();
371 }
372
373 /**
374 * Searches the given path to see if it contains one of the given files.
375 * Returns the first it finds. Returns null if not found or if path is null.
376 *
377 * @param path
378 * List of paths to search separated by File.pathSeparator
379 * @param lookFor
380 * Files to search for in the given path
381 * @return the first match found, or null
382 * @since 3.0
383 **/
384 protected static File searchPath(final String path, final String... lookFor) {
385 if (path == null)
386 return null;
387
388 for (final String p : path.split(File.pathSeparator)) {
389 for (String command : lookFor) {
390 final File e = new File(p, command);
391 if (e.isFile())
392 return e.getAbsoluteFile();
393 }
394 }
395 return null;
396 }
397
398 /**
399 * Execute a command and return a single line of output as a String
400 *
401 * @param dir
402 * Working directory for the command
403 * @param command
404 * as component array
405 * @param encoding
406 * to be used to parse the command's output
407 * @return the one-line output of the command
408 */
409 protected static String readPipe(File dir, String[] command, String encoding) {
410 return readPipe(dir, command, encoding, null);
411 }
412
413 /**
414 * Execute a command and return a single line of output as a String
415 *
416 * @param dir
417 * Working directory for the command
418 * @param command
419 * as component array
420 * @param encoding
421 * to be used to parse the command's output
422 * @param env
423 * Map of environment variables to be merged with those of the
424 * current process
425 * @return the one-line output of the command
426 * @since 4.0
427 */
428 protected static String readPipe(File dir, String[] command, String encoding, Map<String, String> env) {
429 final boolean debug = LOG.isDebugEnabled();
430 try {
431 if (debug) {
432 LOG.debug("readpipe " + Arrays.asList(command) + "," //$NON-NLS-1$ //$NON-NLS-2$
433 + dir);
434 }
435 ProcessBuilder pb = new ProcessBuilder(command);
436 pb.directory(dir);
437 if (env != null) {
438 pb.environment().putAll(env);
439 }
440 Process p = pb.start();
441 BufferedReader lineRead = new BufferedReader(
442 new InputStreamReader(p.getInputStream(), encoding));
443 p.getOutputStream().close();
444 GobblerThread gobbler = new GobblerThread(p, command, dir);
445 gobbler.start();
446 String r = null;
447 try {
448 r = lineRead.readLine();
449 if (debug) {
450 LOG.debug("readpipe may return '" + r + "'"); //$NON-NLS-1$ //$NON-NLS-2$
451 LOG.debug("(ignoring remaing output:"); //$NON-NLS-1$
452 }
453 String l;
454 while ((l = lineRead.readLine()) != null) {
455 if (debug) {
456 LOG.debug(l);
457 }
458 }
459 } finally {
460 p.getErrorStream().close();
461 lineRead.close();
462 }
463
464 for (;;) {
465 try {
466 int rc = p.waitFor();
467 gobbler.join();
468 if (rc == 0 && r != null && r.length() > 0
469 && !gobbler.fail.get())
470 return r;
471 if (debug) {
472 LOG.debug("readpipe rc=" + rc); //$NON-NLS-1$
473 }
474 break;
475 } catch (InterruptedException ie) {
476 // Stop bothering me, I have a zombie to reap.
477 }
478 }
479 } catch (IOException e) {
480 LOG.debug("Caught exception in FS.readPipe()", e); //$NON-NLS-1$
481 }
482 if (debug) {
483 LOG.debug("readpipe returns null"); //$NON-NLS-1$
484 }
485 return null;
486 }
487
488 private static class GobblerThread extends Thread {
489 private final Process p;
490 private final String desc;
491 private final String dir;
492 private final boolean debug = LOG.isDebugEnabled();
493 private final AtomicBoolean fail = new AtomicBoolean();
494
495 private GobblerThread(Process p, String[] command, File dir) {
496 this.p = p;
497 if (debug) {
498 this.desc = Arrays.asList(command).toString();
499 this.dir = dir.toString();
500 } else {
501 this.desc = null;
502 this.dir = null;
503 }
504 }
505
506 public void run() {
507 InputStream is = p.getErrorStream();
508 try {
509 int ch;
510 if (debug) {
511 while ((ch = is.read()) != -1) {
512 System.err.print((char) ch);
513 }
514 } else {
515 while (is.read() != -1) {
516 // ignore
517 }
518 }
519 } catch (IOException e) {
520 logError(e);
521 fail.set(true);
522 }
523 try {
524 is.close();
525 } catch (IOException e) {
526 logError(e);
527 fail.set(true);
528 }
529 }
530
531 private void logError(Throwable t) {
532 if (!debug) {
533 return;
534 }
535 String msg = MessageFormat.format(
536 JGitText.get().exceptionCaughtDuringExcecutionOfCommand, desc, dir);
537 LOG.debug(msg, t);
538 }
539 }
540
541 /**
542 * @return the path to the Git executable or {@code null} if it cannot be
543 * determined.
544 * @since 4.0
545 */
546 protected abstract File discoverGitExe();
547
548 /**
549 * @return the path to the system-wide Git configuration file or
550 * {@code null} if it cannot be determined.
551 * @since 4.0
552 */
553 protected File discoverGitSystemConfig() {
554 File gitExe = discoverGitExe();
555 if (gitExe == null) {
556 return null;
557 }
558
559 // Trick Git into printing the path to the config file by using "echo"
560 // as the editor.
561 Map<String, String> env = new HashMap<>();
562 env.put("GIT_EDITOR", "echo"); //$NON-NLS-1$ //$NON-NLS-2$
563
564 String w = readPipe(gitExe.getParentFile(),
565 new String[] { "git", "config", "--system", "--edit" }, //$NON-NLS-1$ //$NON-NLS-2$ //$NON-NLS-3$ //$NON-NLS-4$
566 Charset.defaultCharset().name(), env);
567 if (StringUtils.isEmptyOrNull(w)) {
568 return null;
569 }
570
571 return new File(w);
572 }
573
574 /**
575 * @return the currently used path to the system-wide Git configuration
576 * file or {@code null} if none has been set.
577 * @since 4.0
578 */
579 public File getGitSystemConfig() {
580 if (gitSystemConfig == null) {
581 gitSystemConfig = new Holder<File>(discoverGitSystemConfig());
582 }
583 return gitSystemConfig.value;
584 }
585
586 /**
587 * Set the path to the system-wide Git configuration file to use.
588 *
589 * @param configFile
590 * the path to the config file.
591 * @return {@code this}
592 * @since 4.0
593 */
594 public FS setGitSystemConfig(File configFile) {
595 gitSystemConfig = new Holder<File>(configFile);
596 return this;
597 }
598
599 /**
600 * @param grandchild
601 * @return the parent directory of this file's parent directory or
602 * {@code null} in case there's no grandparent directory
603 * @since 4.0
604 */
605 protected static File resolveGrandparentFile(File grandchild) {
606 if (grandchild != null) {
607 File parent = grandchild.getParentFile();
608 if (parent != null)
609 return parent.getParentFile();
610 }
611 return null;
612 }
613
614 /**
615 * Check if a file is a symbolic link and read it
616 *
617 * @param path
618 * @return target of link or null
619 * @throws IOException
620 * @since 3.0
621 */
622 public String readSymLink(File path) throws IOException {
623 return FileUtils.readSymLink(path);
624 }
625
626 /**
627 * @param path
628 * @return true if the path is a symbolic link (and we support these)
629 * @throws IOException
630 * @since 3.0
631 */
632 public boolean isSymLink(File path) throws IOException {
633 return FileUtils.isSymlink(path);
634 }
635
636 /**
637 * Tests if the path exists, in case of a symbolic link, true even if the
638 * target does not exist
639 *
640 * @param path
641 * @return true if path exists
642 * @since 3.0
643 */
644 public boolean exists(File path) {
645 return FileUtils.exists(path);
646 }
647
648 /**
649 * Check if path is a directory. If the OS/JRE supports symbolic links and
650 * path is a symbolic link to a directory, this method returns false.
651 *
652 * @param path
653 * @return true if file is a directory,
654 * @since 3.0
655 */
656 public boolean isDirectory(File path) {
657 return FileUtils.isDirectory(path);
658 }
659
660 /**
661 * Examine if path represents a regular file. If the OS/JRE supports
662 * symbolic links the test returns false if path represents a symbolic link.
663 *
664 * @param path
665 * @return true if path represents a regular file
666 * @since 3.0
667 */
668 public boolean isFile(File path) {
669 return FileUtils.isFile(path);
670 }
671
672 /**
673 * @param path
674 * @return true if path is hidden, either starts with . on unix or has the
675 * hidden attribute in windows
676 * @throws IOException
677 * @since 3.0
678 */
679 public boolean isHidden(File path) throws IOException {
680 return FileUtils.isHidden(path);
681 }
682
683 /**
684 * Set the hidden attribute for file whose name starts with a period.
685 *
686 * @param path
687 * @param hidden
688 * @throws IOException
689 * @since 3.0
690 */
691 public void setHidden(File path, boolean hidden) throws IOException {
692 FileUtils.setHidden(path, hidden);
693 }
694
695 /**
696 * Create a symbolic link
697 *
698 * @param path
699 * @param target
700 * @throws IOException
701 * @since 3.0
702 */
703 public void createSymLink(File path, String target) throws IOException {
704 FileUtils.createSymLink(path, target);
705 }
706
707 /**
708 * See {@link FileUtils#relativize(String, String)}.
709 *
710 * @param base
711 * The path against which <code>other</code> should be
712 * relativized.
713 * @param other
714 * The path that will be made relative to <code>base</code>.
715 * @return A relative path that, when resolved against <code>base</code>,
716 * will yield the original <code>other</code>.
717 * @see FileUtils#relativize(String, String)
718 * @since 3.7
719 */
720 public String relativize(String base, String other) {
721 return FileUtils.relativize(base, other);
722 }
723
724 /**
725 * Checks whether the given hook is defined for the given repository, then
726 * runs it with the given arguments.
727 * <p>
728 * The hook's standard output and error streams will be redirected to
729 * <code>System.out</code> and <code>System.err</code> respectively. The
730 * hook will have no stdin.
731 * </p>
732 *
733 * @param repository
734 * The repository for which a hook should be run.
735 * @param hookName
736 * The name of the hook to be executed.
737 * @param args
738 * Arguments to pass to this hook. Cannot be <code>null</code>,
739 * but can be an empty array.
740 * @return The ProcessResult describing this hook's execution.
741 * @throws JGitInternalException
742 * if we fail to run the hook somehow. Causes may include an
743 * interrupted process or I/O errors.
744 * @since 4.0
745 */
746 public ProcessResult runHookIfPresent(Repository repository,
747 final String hookName,
748 String[] args) throws JGitInternalException {
749 return runHookIfPresent(repository, hookName, args, System.out, System.err,
750 null);
751 }
752
753 /**
754 * Checks whether the given hook is defined for the given repository, then
755 * runs it with the given arguments.
756 *
757 * @param repository
758 * The repository for which a hook should be run.
759 * @param hookName
760 * The name of the hook to be executed.
761 * @param args
762 * Arguments to pass to this hook. Cannot be <code>null</code>,
763 * but can be an empty array.
764 * @param outRedirect
765 * A print stream on which to redirect the hook's stdout. Can be
766 * <code>null</code>, in which case the hook's standard output
767 * will be lost.
768 * @param errRedirect
769 * A print stream on which to redirect the hook's stderr. Can be
770 * <code>null</code>, in which case the hook's standard error
771 * will be lost.
772 * @param stdinArgs
773 * A string to pass on to the standard input of the hook. May be
774 * <code>null</code>.
775 * @return The ProcessResult describing this hook's execution.
776 * @throws JGitInternalException
777 * if we fail to run the hook somehow. Causes may include an
778 * interrupted process or I/O errors.
779 * @since 4.0
780 */
781 public ProcessResult runHookIfPresent(Repository repository,
782 final String hookName,
783 String[] args, PrintStream outRedirect, PrintStream errRedirect,
784 String stdinArgs) throws JGitInternalException {
785 return new ProcessResult(Status.NOT_SUPPORTED);
786 }
787
788 /**
789 * See
790 * {@link #runHookIfPresent(Repository, String, String[], PrintStream, PrintStream, String)}
791 * . Should only be called by FS supporting shell scripts execution.
792 *
793 * @param repository
794 * The repository for which a hook should be run.
795 * @param hookName
796 * The name of the hook to be executed.
797 * @param args
798 * Arguments to pass to this hook. Cannot be <code>null</code>,
799 * but can be an empty array.
800 * @param outRedirect
801 * A print stream on which to redirect the hook's stdout. Can be
802 * <code>null</code>, in which case the hook's standard output
803 * will be lost.
804 * @param errRedirect
805 * A print stream on which to redirect the hook's stderr. Can be
806 * <code>null</code>, in which case the hook's standard error
807 * will be lost.
808 * @param stdinArgs
809 * A string to pass on to the standard input of the hook. May be
810 * <code>null</code>.
811 * @return The ProcessResult describing this hook's execution.
812 * @throws JGitInternalException
813 * if we fail to run the hook somehow. Causes may include an
814 * interrupted process or I/O errors.
815 * @since 4.0
816 */
817 protected ProcessResult internalRunHookIfPresent(Repository repository,
818 final String hookName, String[] args, PrintStream outRedirect,
819 PrintStream errRedirect, String stdinArgs)
820 throws JGitInternalException {
821 final File hookFile = findHook(repository, hookName);
822 if (hookFile == null)
823 return new ProcessResult(Status.NOT_PRESENT);
824
825 final String hookPath = hookFile.getAbsolutePath();
826 final File runDirectory;
827 if (repository.isBare())
828 runDirectory = repository.getDirectory();
829 else
830 runDirectory = repository.getWorkTree();
831 final String cmd = relativize(runDirectory.getAbsolutePath(),
832 hookPath);
833 ProcessBuilder hookProcess = runInShell(cmd, args);
834 hookProcess.directory(runDirectory);
835 try {
836 return new ProcessResult(runProcess(hookProcess, outRedirect,
837 errRedirect, stdinArgs), Status.OK);
838 } catch (IOException e) {
839 throw new JGitInternalException(MessageFormat.format(
840 JGitText.get().exceptionCaughtDuringExecutionOfHook,
841 hookName), e);
842 } catch (InterruptedException e) {
843 throw new JGitInternalException(MessageFormat.format(
844 JGitText.get().exceptionHookExecutionInterrupted,
845 hookName), e);
846 }
847 }
848
849
850 /**
851 * Tries to find a hook matching the given one in the given repository.
852 *
853 * @param repository
854 * The repository within which to find a hook.
855 * @param hookName
856 * The name of the hook we're trying to find.
857 * @return The {@link File} containing this particular hook if it exists in
858 * the given repository, <code>null</code> otherwise.
859 * @since 4.0
860 */
861 public File findHook(Repository repository, final String hookName) {
862 File gitDir = repository.getDirectory();
863 if (gitDir == null)
864 return null;
865 final File hookFile = new File(new File(gitDir,
866 Constants.HOOKS), hookName);
867 return hookFile.isFile() ? hookFile : null;
868 }
869
870 /**
871 * Runs the given process until termination, clearing its stdout and stderr
872 * streams on-the-fly.
873 *
874 * @param hookProcessBuilder
875 * The process builder configured for this hook.
876 * @param outRedirect
877 * A print stream on which to redirect the hook's stdout. Can be
878 * <code>null</code>, in which case the hook's standard output
879 * will be lost.
880 * @param errRedirect
881 * A print stream on which to redirect the hook's stderr. Can be
882 * <code>null</code>, in which case the hook's standard error
883 * will be lost.
884 * @param stdinArgs
885 * A string to pass on to the standard input of the hook. Can be
886 * <code>null</code>.
887 * @return the exit value of this hook.
888 * @throws IOException
889 * if an I/O error occurs while executing this hook.
890 * @throws InterruptedException
891 * if the current thread is interrupted while waiting for the
892 * process to end.
893 * @since 3.7
894 */
895 protected int runProcess(ProcessBuilder hookProcessBuilder,
896 OutputStream outRedirect, OutputStream errRedirect, String stdinArgs)
897 throws IOException, InterruptedException {
898 final ExecutorService executor = Executors.newFixedThreadPool(2);
899 Process process = null;
900 // We'll record the first I/O exception that occurs, but keep on trying
901 // to dispose of our open streams and file handles
902 IOException ioException = null;
903 try {
904 process = hookProcessBuilder.start();
905 final Callable<Void> errorGobbler = new StreamGobbler(
906 process.getErrorStream(), errRedirect);
907 final Callable<Void> outputGobbler = new StreamGobbler(
908 process.getInputStream(), outRedirect);
909 executor.submit(errorGobbler);
910 executor.submit(outputGobbler);
911 if (stdinArgs != null) {
912 final PrintWriter stdinWriter = new PrintWriter(
913 process.getOutputStream());
914 stdinWriter.print(stdinArgs);
915 stdinWriter.flush();
916 // We are done with this hook's input. Explicitly close its
917 // stdin now to kick off any blocking read the hook might have.
918 stdinWriter.close();
919 }
920 return process.waitFor();
921 } catch (IOException e) {
922 ioException = e;
923 } finally {
924 shutdownAndAwaitTermination(executor);
925 if (process != null) {
926 try {
927 process.waitFor();
928 } catch (InterruptedException e) {
929 // Thrown by the outer try.
930 // Swallow this one to carry on our cleanup, and clear the
931 // interrupted flag (processes throw the exception without
932 // clearing the flag).
933 Thread.interrupted();
934 }
935 // A process doesn't clean its own resources even when destroyed
936 // Explicitly try and close all three streams, preserving the
937 // outer I/O exception if any.
938 try {
939 process.getErrorStream().close();
940 } catch (IOException e) {
941 ioException = ioException != null ? ioException : e;
942 }
943 try {
944 process.getInputStream().close();
945 } catch (IOException e) {
946 ioException = ioException != null ? ioException : e;
947 }
948 try {
949 process.getOutputStream().close();
950 } catch (IOException e) {
951 ioException = ioException != null ? ioException : e;
952 }
953 process.destroy();
954 }
955 }
956 // We can only be here if the outer try threw an IOException.
957 throw ioException;
958 }
959
960 /**
961 * Shuts down an {@link ExecutorService} in two phases, first by calling
962 * {@link ExecutorService#shutdown() shutdown} to reject incoming tasks, and
963 * then calling {@link ExecutorService#shutdownNow() shutdownNow}, if
964 * necessary, to cancel any lingering tasks. Returns true if the pool has
965 * been properly shutdown, false otherwise.
966 * <p>
967 *
968 * @param pool
969 * the pool to shutdown
970 * @return <code>true</code> if the pool has been properly shutdown,
971 * <code>false</code> otherwise.
972 */
973 private static boolean shutdownAndAwaitTermination(ExecutorService pool) {
974 boolean hasShutdown = true;
975 pool.shutdown(); // Disable new tasks from being submitted
976 try {
977 // Wait a while for existing tasks to terminate
978 if (!pool.awaitTermination(5, TimeUnit.SECONDS)) {
979 pool.shutdownNow(); // Cancel currently executing tasks
980 // Wait a while for tasks to respond to being canceled
981 if (!pool.awaitTermination(5, TimeUnit.SECONDS))
982 hasShutdown = false;
983 }
984 } catch (InterruptedException ie) {
985 // (Re-)Cancel if current thread also interrupted
986 pool.shutdownNow();
987 // Preserve interrupt status
988 Thread.currentThread().interrupt();
989 hasShutdown = false;
990 }
991 return hasShutdown;
992 }
993
994 /**
995 * Initialize a ProcessBuilder to run a command using the system shell.
996 *
997 * @param cmd
998 * command to execute. This string should originate from the
999 * end-user, and thus is platform specific.
1000 * @param args
1001 * arguments to pass to command. These should be protected from
1002 * shell evaluation.
1003 * @return a partially completed process builder. Caller should finish
1004 * populating directory, environment, and then start the process.
1005 */
1006 public abstract ProcessBuilder runInShell(String cmd, String[] args);
1007
1008 private static class Holder<V> {
1009 final V value;
1010
1011 Holder(V value) {
1012 this.value = value;
1013 }
1014 }
1015
1016 /**
1017 * File attributes we typically care for.
1018 *
1019 * @since 3.3
1020 */
1021 public static class Attributes {
1022
1023 /**
1024 * @return true if this are the attributes of a directory
1025 */
1026 public boolean isDirectory() {
1027 return isDirectory;
1028 }
1029
1030 /**
1031 * @return true if this are the attributes of an executable file
1032 */
1033 public boolean isExecutable() {
1034 return isExecutable;
1035 }
1036
1037 /**
1038 * @return true if this are the attributes of a symbolic link
1039 */
1040 public boolean isSymbolicLink() {
1041 return isSymbolicLink;
1042 }
1043
1044 /**
1045 * @return true if this are the attributes of a regular file
1046 */
1047 public boolean isRegularFile() {
1048 return isRegularFile;
1049 }
1050
1051 /**
1052 * @return the time when the file was created
1053 */
1054 public long getCreationTime() {
1055 return creationTime;
1056 }
1057
1058 /**
1059 * @return the time (milliseconds since 1970-01-01) when this object was
1060 * last modified
1061 */
1062 public long getLastModifiedTime() {
1063 return lastModifiedTime;
1064 }
1065
1066 private final boolean isDirectory;
1067
1068 private final boolean isSymbolicLink;
1069
1070 private final boolean isRegularFile;
1071
1072 private final long creationTime;
1073
1074 private final long lastModifiedTime;
1075
1076 private final boolean isExecutable;
1077
1078 private final File file;
1079
1080 private final boolean exists;
1081
1082 /**
1083 * file length
1084 */
1085 protected long length = -1;
1086
1087 final FS fs;
1088
1089 Attributes(FS fs, File file, boolean exists, boolean isDirectory,
1090 boolean isExecutable, boolean isSymbolicLink,
1091 boolean isRegularFile, long creationTime,
1092 long lastModifiedTime, long length) {
1093 this.fs = fs;
1094 this.file = file;
1095 this.exists = exists;
1096 this.isDirectory = isDirectory;
1097 this.isExecutable = isExecutable;
1098 this.isSymbolicLink = isSymbolicLink;
1099 this.isRegularFile = isRegularFile;
1100 this.creationTime = creationTime;
1101 this.lastModifiedTime = lastModifiedTime;
1102 this.length = length;
1103 }
1104
1105 /**
1106 * Constructor when there are issues with reading. All attributes except
1107 * given will be set to the default values.
1108 *
1109 * @param fs
1110 * @param path
1111 */
1112 public Attributes(File path, FS fs) {
1113 this(fs, path, false, false, false, false, false, 0L, 0L, 0L);
1114 }
1115
1116 /**
1117 * @return length of this file object
1118 */
1119 public long getLength() {
1120 if (length == -1)
1121 return length = file.length();
1122 return length;
1123 }
1124
1125 /**
1126 * @return the filename
1127 */
1128 public String getName() {
1129 return file.getName();
1130 }
1131
1132 /**
1133 * @return the file the attributes apply to
1134 */
1135 public File getFile() {
1136 return file;
1137 }
1138
1139 boolean exists() {
1140 return exists;
1141 }
1142 }
1143
1144 /**
1145 * @param path
1146 * @return the file attributes we care for
1147 * @since 3.3
1148 */
1149 public Attributes getAttributes(File path) {
1150 boolean isDirectory = isDirectory(path);
1151 boolean isFile = !isDirectory && path.isFile();
1152 assert path.exists() == isDirectory || isFile;
1153 boolean exists = isDirectory || isFile;
1154 boolean canExecute = exists && !isDirectory && canExecute(path);
1155 boolean isSymlink = false;
1156 long lastModified = exists ? path.lastModified() : 0L;
1157 long createTime = 0L;
1158 return new Attributes(this, path, exists, isDirectory, canExecute,
1159 isSymlink, isFile, createTime, lastModified, -1);
1160 }
1161
1162 /**
1163 * Normalize the unicode path to composed form.
1164 *
1165 * @param file
1166 * @return NFC-format File
1167 * @since 3.3
1168 */
1169 public File normalize(File file) {
1170 return file;
1171 }
1172
1173 /**
1174 * Normalize the unicode path to composed form.
1175 *
1176 * @param name
1177 * @return NFC-format string
1178 * @since 3.3
1179 */
1180 public String normalize(String name) {
1181 return name;
1182 }
1183
1184 /**
1185 * This runnable will consume an input stream's content into an output
1186 * stream as soon as it gets available.
1187 * <p>
1188 * Typically used to empty processes' standard output and error, preventing
1189 * them to choke.
1190 * </p>
1191 * <p>
1192 * <b>Note</b> that a {@link StreamGobbler} will never close either of its
1193 * streams.
1194 * </p>
1195 */
1196 private static class StreamGobbler implements Callable<Void> {
1197 private final BufferedReader reader;
1198
1199 private final BufferedWriter writer;
1200
1201 public StreamGobbler(InputStream stream, OutputStream output) {
1202 this.reader = new BufferedReader(new InputStreamReader(stream));
1203 if (output == null)
1204 this.writer = null;
1205 else
1206 this.writer = new BufferedWriter(new OutputStreamWriter(output));
1207 }
1208
1209 public Void call() throws IOException {
1210 boolean writeFailure = false;
1211
1212 String line = null;
1213 while ((line = reader.readLine()) != null) {
1214 // Do not try to write again after a failure, but keep reading
1215 // as long as possible to prevent the input stream from choking.
1216 if (!writeFailure && writer != null) {
1217 try {
1218 writer.write(line);
1219 writer.newLine();
1220 writer.flush();
1221 } catch (IOException e) {
1222 writeFailure = true;
1223 }
1224 }
1225 }
1226 return null;
1227 }
1228 }
1229 }