#!/usr/bin/env php
<?php

// NOTE: This script will sometimes emit a warning like this on startup:
//
//   No entry for terminal type "unknown";
//   using dumb terminal settings.
//
// This can be fixed by adding "TERM=dumb" to the shebang line, but doing so
// causes some systems to hang mysteriously. See T7119.

// Commit hooks execute in an unusual context where the environment may be
// unavailable, particularly in SVN. The first parameter to this script is
// either a bare repository identifier ("X"), or a repository identifier
// followed by an instance identifier ("X:instance"). If we have an instance
// identifier, unpack it into the environment before we start up. This allows
// subclasses of PhabricatorConfigSiteSource to read it and build an instance
// environment.

$hook_start = microtime(true);

if ($argc > 1) {
  $context = $argv[1];
  $context = explode(':', $context, 2);
  $argv[1] = $context[0];

  if (count($context) > 1) {
    $_ENV['PHABRICATOR_INSTANCE'] = $context[1];
    putenv('PHABRICATOR_INSTANCE='.$context[1]);
  }
}

$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';

if ($argc < 2) {
  throw new Exception(pht('usage: commit-hook <repository>'));
}

$engine = id(new DiffusionCommitHookEngine())
  ->setStartTime($hook_start);

$repository = id(new PhabricatorRepositoryQuery())
  ->setViewer(PhabricatorUser::getOmnipotentUser())
  ->withIdentifiers(array($argv[1]))
  ->needProjectPHIDs(true)
  ->executeOne();

if (!$repository) {
  throw new Exception(pht('No such repository "%s"!', $argv[1]));
}

if (!$repository->isHosted()) {
  // In Mercurial, the "pretxnchangegroup" hook fires for both pulls and
  // pushes. Normally we only install the hook for hosted repositories, but
  // if a hosted repository is later converted into an observed repository we
  // can end up with an observed repository that has the hook installed.
  // If we're running hooks from an observed repository, just exit without
  // taking action. For more discussion, see PHI24.
  return 0;
}

$engine->setRepository($repository);

$args = new PhutilArgumentParser($argv);
$args->parsePartial(
  array(
    array(
      'name' => 'hook-mode',
      'param' => 'mode',
      'help' => pht('Hook execution mode.'),
    ),
  ));

$argv = array_merge(
  array($argv[0]),
  $args->getUnconsumedArgumentVector());

// Figure out which user is writing the commit.
$hook_mode = $args->getArg('hook-mode');
if ($hook_mode !== null) {
  $known_modes = array(
    'svn-revprop' => true,
  );

  if (empty($known_modes[$hook_mode])) {
    throw new Exception(
      pht(
        'Invalid Hook Mode: This hook was invoked in "%s" mode, but this '.
        'is not a recognized hook mode. Valid modes are: %s.',
        $hook_mode,
        implode(', ', array_keys($known_modes))));
  }
}

$is_svnrevprop = ($hook_mode == 'svn-revprop');

if ($is_svnrevprop) {
  // For now, we let these through if the repository allows dangerous changes
  // and prevent them if it doesn't. See T11208 for discussion.

  $revprop_key = $argv[5];

  if ($repository->shouldAllowDangerousChanges()) {
    $err = 0;
  } else {
    $err = 1;

    $console = PhutilConsole::getConsole();
    $console->writeErr(
      pht(
        "DANGEROUS CHANGE: Dangerous change protection is enabled for this ".
        "repository, so you can not change revision properties (you are ".
        "attempting to edit \"%s\").\n".
        "Edit the repository configuration before making dangerous changes.",
        $revprop_key));
  }

  exit($err);
} else if ($repository->isGit() || $repository->isHg()) {
  $username = getenv(DiffusionCommitHookEngine::ENV_USER);
  if (!strlen($username)) {
    throw new Exception(
      pht(
        'No Direct Pushes: You are pushing directly to a hosted repository. '.
        'This will not work. See "No Direct Pushes" in the documentation '.
        'for more information.'));
  }

  if ($repository->isHg()) {
    // We respond to several different hooks in Mercurial.
    $engine->setMercurialHook($argv[2]);
  }

} else if ($repository->isSVN()) {
  // NOTE: In Subversion, the entire environment gets wiped so we can't read
  // DiffusionCommitHookEngine::ENV_USER. Instead, we've set "--tunnel-user" to
  // specify the correct user; read this user out of the commit log.

  if ($argc < 4) {
    throw new Exception(pht('usage: commit-hook <repository> <repo> <txn>'));
  }

  $svn_repo = $argv[2];
  $svn_txn = $argv[3];
  list($username) = execx('svnlook author -t %s %s', $svn_txn, $svn_repo);
  $username = rtrim($username, "\n");

  $engine->setSubversionTransactionInfo($svn_txn, $svn_repo);
} else {
  throw new Exception(pht('Unknown repository type.'));
}

$user = id(new PhabricatorPeopleQuery())
  ->setViewer(PhabricatorUser::getOmnipotentUser())
  ->withUsernames(array($username))
  ->executeOne();

if (!$user) {
  throw new Exception(pht('No such user "%s"!', $username));
}

$engine->setViewer($user);


// Read stdin for the hook engine.

if ($repository->isHg()) {
  // Mercurial leaves stdin open, so we can't just read it until EOF.
  $stdin = '';
} else {
  // Git and Subversion write data into stdin and then close it. Read the
  // data.
  $stdin = @file_get_contents('php://stdin');
  if ($stdin === false) {
    throw new Exception(pht('Failed to read stdin!'));
  }
}

$engine->setStdin($stdin);
$engine->setOriginalArgv(array_slice($argv, 2));

$remote_address = getenv(DiffusionCommitHookEngine::ENV_REMOTE_ADDRESS);
if (strlen($remote_address)) {
  $engine->setRemoteAddress($remote_address);
}

$remote_protocol = getenv(DiffusionCommitHookEngine::ENV_REMOTE_PROTOCOL);
if (strlen($remote_protocol)) {
  $engine->setRemoteProtocol($remote_protocol);
}

$request_identifier = getenv(DiffusionCommitHookEngine::ENV_REQUEST);
if (strlen($request_identifier)) {
  $engine->setRequestIdentifier($request_identifier);
}

try {
  $err = $engine->execute();
} catch (DiffusionCommitHookRejectException $ex) {
  $console = PhutilConsole::getConsole();

  if (PhabricatorEnv::getEnvConfig('phabricator.serious-business')) {
    $preamble = pht('*** PUSH REJECTED BY COMMIT HOOK ***');
  } else {
    $preamble = pht(<<<EOTXT
+---------------------------------------------------------------+
|      * * * PUSH REJECTED BY EVIL DRAGON BUREAUCRATS * * *     |
+---------------------------------------------------------------+
             \
              \                    ^    /^
               \                  / \  // \
                \   |\___/|      /   \//  .\
                 \  /V  V  \__  /    //  | \ \           *----*
                   /     /  \/_/    //   |  \  \          \   |
                   @___@`    \/_   //    |   \   \         \/\ \
                  0/0/|       \/_ //     |    \    \         \  \
              0/0/0/0/|        \///      |     \     \       |  |
           0/0/0/0/0/_|_ /   (  //       |      \     _\     |  /
        0/0/0/0/0/0/`/,_ _ _/  ) ; -.    |    _ _\.-~       /   /
                    ,-}        _      *-.|.-~-.           .~    ~
  *     \__/         `/\      /                 ~-. _ .-~      /
   \____(Oo)            *.   }            {                   /
   (    (..)           .----~-.\        \-`                 .~
   //___\\\\  \ DENIED!  ///.----..<        \             _ -~
  //     \\\\                ///-._ _ _ _ _ _ _{^ - - - - ~

EOTXT
);
  }

  $console->writeErr("%s\n\n", $preamble);
  $console->writeErr("%s\n\n", $ex->getMessage());
  $err = 1;
}

exit($err);
