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