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