diff --git a/src/applications/conduit/method/repository/ConduitAPI_repository_create_Method.php b/src/applications/conduit/method/repository/ConduitAPI_repository_create_Method.php
index e251d6f8a0..e0a5146f50 100644
--- a/src/applications/conduit/method/repository/ConduitAPI_repository_create_Method.php
+++ b/src/applications/conduit/method/repository/ConduitAPI_repository_create_Method.php
@@ -1,138 +1,142 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 /**
  * @group conduit
  */
 final class ConduitAPI_repository_create_Method
   extends ConduitAPI_repository_Method {
 
   public function getMethodStatus() {
     return self::METHOD_STATUS_UNSTABLE;
   }
 
   public function getMethodStatusDescription() {
     return "Repository methods are new and subject to change.";
   }
 
   public function getMethodDescription() {
     return "Create a new repository (Admin Only).";
   }
 
   public function defineParamTypes() {
     return array(
-      'name'          => 'required string',
-      'vcs'           => 'required enum<git, hg, svn>',
-      'callsign'      => 'required string',
-      'encoding'      => 'optional string',
-      'tracking'      => 'optional bool',
-      'uri'           => 'optional string',
-      'sshUser'       => 'optional string',
-      'sshKey'        => 'optional string',
-      'sshKeyFile'    => 'optional string',
-      'httpUser'      => 'optional string',
-      'httpPassword'  => 'optional string',
-      'localPath'     => 'optional string',
-      'svnSubpath'    => 'optional string',
-      'branchFilter'  => 'optional list<string>',
-      'pullFrequency' => 'optional int',
-      'defaultBranch' => 'optional string',
-      'heraldEnabled' => 'optional bool',
-      'svnUUID'       => 'optional string',
+      'name'                => 'required string',
+      'vcs'                 => 'required enum<git, hg, svn>',
+      'callsign'            => 'required string',
+      'encoding'            => 'optional string',
+      'tracking'            => 'optional bool',
+      'uri'                 => 'optional string',
+      'sshUser'             => 'optional string',
+      'sshKey'              => 'optional string',
+      'sshKeyFile'          => 'optional string',
+      'httpUser'            => 'optional string',
+      'httpPassword'        => 'optional string',
+      'localPath'           => 'optional string',
+      'svnSubpath'          => 'optional string',
+      'branchFilter'        => 'optional list<string>',
+      'closeCommitsFilter'  => 'optional list<string>',
+      'pullFrequency'       => 'optional int',
+      'defaultBranch'       => 'optional string',
+      'heraldEnabled'       => 'optional bool',
+      'svnUUID'             => 'optional string',
     );
   }
 
   public function defineReturnType() {
     return 'nonempty dict';
   }
 
   public function defineErrorTypes() {
     return array(
       'ERR-PERMISSIONS' =>
         'You do not have the authority to call this method.',
       'ERR-DUPLICATE'   =>
         'Duplicate repository callsign.',
       'ERR-BAD-CALLSIGN' =>
         'Callsign is required and must be ALL UPPERCASE LETTERS.',
       'ERR-UNKNOWN-REPOSITORY-VCS' =>
         'Unknown repository VCS type.',
     );
   }
 
   protected function execute(ConduitAPIRequest $request) {
     if (!$request->getUser()->getIsAdmin()) {
       throw new ConduitException('ERR-PERMISSIONS');
     }
 
     // TODO: This has some duplication with (and lacks some of the validation
     // of) the web workflow; refactor things so they can share more code as this
     // stabilizes.
 
     $repository = new PhabricatorRepository();
     $repository->setName($request->getValue('name'));
 
     $callsign = $request->getValue('callsign');
     if (!preg_match('/[A-Z]+$/', $callsign)) {
       throw new ConduitException('ERR-BAD-CALLSIGN');
     }
     $repository->setCallsign($callsign);
 
     $vcs = $request->getValue('vcs');
 
     $map = array(
       'git' => PhabricatorRepositoryType::REPOSITORY_TYPE_GIT,
       'hg'  => PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL,
       'svn' => PhabricatorRepositoryType::REPOSITORY_TYPE_SVN,
     );
     if (empty($map[$vcs])) {
       throw new ConduitException('ERR-UNKNOWN-REPOSITORY-VCS');
     }
     $repository->setVersionControlSystem($map[$vcs]);
 
     $details = array(
       'encoding'          => $request->getValue('encoding'),
       'tracking-enabled'  => (bool)$request->getValue('tracking', true),
       'remote-uri'        => $request->getValue('uri'),
       'local-path'        => $request->getValue('localPath'),
       'branch-filter'     => array_fill_keys(
         $request->getValue('branchFilter', array()),
         true),
+      'close-commits-filter' => array_fill_keys(
+        $request->getValue('closeCommitsFilter', array()),
+        true),
       'pull-frequency'    => $request->getValue('pullFrequency'),
       'default-branch'    => $request->getValue('defaultBranch'),
       'ssh-login'         => $request->getValue('sshUser'),
       'ssh-key'           => $request->getValue('sshKey'),
       'ssh-keyfile'       => $request->getValue('sshKeyFile'),
       'herald-disabled'   => !$request->getValue('heraldEnabled', true),
       'svn-subpath'       => $request->getValue('svnSubpath'),
     );
 
     foreach ($details as $key => $value) {
       $repository->setDetail($key, $value);
     }
 
     try {
       $repository->save();
     } catch (AphrontQueryDuplicateKeyException $ex) {
       throw new ConduitException('ERR-DUPLICATE');
     }
 
     return $this->buildDictForRepository($repository);
   }
 
 
 }
diff --git a/src/applications/repository/controller/PhabricatorRepositoryEditController.php b/src/applications/repository/controller/PhabricatorRepositoryEditController.php
index ae5cdfd0d6..3905d04784 100644
--- a/src/applications/repository/controller/PhabricatorRepositoryEditController.php
+++ b/src/applications/repository/controller/PhabricatorRepositoryEditController.php
@@ -1,702 +1,722 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 final class PhabricatorRepositoryEditController
   extends PhabricatorRepositoryController {
 
   private $id;
   private $view;
   private $repository;
   private $sideNav;
 
   public function willProcessRequest(array $data) {
     $this->id = $data['id'];
     $this->view = idx($data, 'view');
   }
 
   public function processRequest() {
 
     $request = $this->getRequest();
 
     $repository = id(new PhabricatorRepository())->load($this->id);
     if (!$repository) {
       return new Aphront404Response();
     }
 
     $views = array(
       'basic'     => 'Basics',
       'tracking'  => 'Tracking',
     );
 
     $this->repository = $repository;
 
     if (!isset($views[$this->view])) {
       $this->view = head_key($views);
     }
 
     $nav = new AphrontSideNavView();
     foreach ($views as $view => $name) {
       $nav->addNavItem(
         phutil_render_tag(
           'a',
           array(
             'class' => ($view == $this->view
               ? 'aphront-side-nav-selected'
               : null),
             'href'  => '/repository/edit/'.$repository->getID().'/'.$view.'/',
           ),
           phutil_escape_html($name)));
     }
 
     $nav->appendChild($this->renderDaemonNotice());
 
     $this->sideNav = $nav;
 
     switch ($this->view) {
       case 'basic':
         return $this->processBasicRequest();
       case 'tracking':
         return $this->processTrackingRequest();
       default:
         throw new Exception("Unknown view.");
     }
   }
 
   protected function processBasicRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
     $repository = $this->repository;
     $repository_id = $repository->getID();
 
     $errors = array();
 
     $e_name = true;
 
     if ($request->isFormPost()) {
       $repository->setName($request->getStr('name'));
 
       if (!strlen($repository->getName())) {
         $e_name = 'Required';
         $errors[] = 'Repository name is required.';
       } else {
         $e_name = null;
       }
 
       $repository->setDetail('description', $request->getStr('description'));
       $repository->setDetail('encoding', $request->getStr('encoding'));
 
       if (!$errors) {
         $repository->save();
         return id(new AphrontRedirectResponse())
           ->setURI('/repository/edit/'.$repository_id.'/basic/?saved=true');
       }
     }
 
     $error_view = null;
     if ($errors) {
       $error_view = new AphrontErrorView();
       $error_view->setErrors($errors);
       $error_view->setTitle('Form Errors');
     } else if ($request->getStr('saved')) {
       $error_view = new AphrontErrorView();
       $error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
       $error_view->setTitle('Changes Saved');
       $error_view->appendChild(
         'Repository changes were saved.');
     }
 
     $encoding_doc_link = PhabricatorEnv::getDoclink(
         'article/User_Guide_UTF-8_and_Character_Encoding.html');
 
     $form = new AphrontFormView();
     $form
       ->setUser($user)
       ->setAction('/repository/edit/'.$repository->getID().'/')
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel('Name')
           ->setName('name')
           ->setValue($repository->getName())
           ->setError($e_name)
           ->setCaption('Human-readable repository name.'))
       ->appendChild(
         id(new AphrontFormTextAreaControl())
           ->setLabel('Description')
           ->setName('description')
           ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
           ->setValue($repository->getDetail('description')))
       ->appendChild(
         id(new AphrontFormStaticControl())
           ->setLabel('Callsign')
           ->setName('callsign')
           ->setValue($repository->getCallsign()))
       ->appendChild('
         <p class="aphront-form-instructions">'.
           'If source code in this repository uses a character '.
           'encoding other than UTF-8 (for example, ISO-8859-1), '.
           'specify it here. You can usually leave this field blank. '.
           'See User Guide: '.
           '<a href="'.$encoding_doc_link.'">'.
             'UTF-8 and Character Encoding'.
           '</a> for more information.'.
         '</p>')
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setLabel('Encoding')
           ->setName('encoding')
           ->setValue($repository->getDetail('encoding')))
       ->appendChild(
         id(new AphrontFormStaticControl())
           ->setLabel('Type')
           ->setName('type')
           ->setValue($repository->getVersionControlSystem()))
       ->appendChild(
         id(new AphrontFormStaticControl())
           ->setLabel('ID')
           ->setValue($repository->getID()))
       ->appendChild(
         id(new AphrontFormStaticControl())
           ->setLabel('PHID')
           ->setValue($repository->getPHID()))
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue('Save'));
 
     $panel = new AphrontPanelView();
     $panel->setHeader('Edit Repository');
     $panel->appendChild($form);
     $panel->setWidth(AphrontPanelView::WIDTH_FORM);
 
 
     $nav = $this->sideNav;
 
     $nav->appendChild($error_view);
     $nav->appendChild($panel);
 
     return $this->buildStandardPageResponse(
       $nav,
       array(
         'title' => 'Edit Repository',
       ));
   }
 
   private function processTrackingRequest() {
     $request = $this->getRequest();
     $user = $request->getUser();
     $repository = $this->repository;
     $repository_id = $repository->getID();
 
     $errors = array();
 
     $e_uri = null;
     $e_path = null;
 
     $is_git = false;
     $is_svn = false;
     $is_mercurial = false;
 
     $e_ssh_key = null;
     $e_ssh_keyfile = null;
     $e_branch = null;
 
     switch ($repository->getVersionControlSystem()) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         $is_git = true;
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         $is_svn = true;
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $is_mercurial = true;
         break;
       default:
         throw new Exception("Unsupported VCS!");
     }
 
     $has_branches       = ($is_git || $is_mercurial);
     $has_local          = ($is_git || $is_mercurial);
     $has_branch_filter  = ($is_git);
     $has_auth_support   = $is_svn;
 
     if ($request->isFormPost()) {
       $tracking = ($request->getStr('tracking') == 'enabled' ? true : false);
       $repository->setDetail('tracking-enabled', $tracking);
       $repository->setDetail('remote-uri', $request->getStr('uri'));
       if ($has_local) {
         $repository->setDetail('local-path', $request->getStr('path'));
       }
 
       if ($has_branch_filter) {
         $branch_filter = $request->getStrList('branch-filter');
         $branch_filter = array_fill_keys($branch_filter, true);
 
         $repository->setDetail('branch-filter', $branch_filter);
+
+        $close_commits_filter = $request->getStrList('close-commits-filter');
+        $close_commits_filter = array_fill_keys($close_commits_filter, true);
+
+        $repository->setDetail('close-commits-filter', $close_commits_filter);
       }
 
       $repository->setDetail(
         'disable-autoclose',
         $request->getStr('autoclose') == 'disabled' ? true : false);
 
       $repository->setDetail(
         'pull-frequency',
         max(1, $request->getInt('frequency')));
 
       if ($has_branches) {
         $repository->setDetail(
           'default-branch',
           $request->getStr('default-branch'));
         if ($is_git) {
           $branch_name = $repository->getDetail('default-branch');
           if (strpos($branch_name, '/') !== false) {
             $e_branch = 'Invalid';
             $errors[] = "Your branch name should not specify an explicit ".
                         "remote. For instance, use 'master', not ".
                         "'origin/master'.";
           }
         }
       }
 
       $repository->setDetail(
         'default-owners-path',
         $request->getStr(
           'default-owners-path',
           '/'));
 
       $repository->setDetail('ssh-login', $request->getStr('ssh-login'));
       $repository->setDetail('ssh-key', $request->getStr('ssh-key'));
       $repository->setDetail('ssh-keyfile', $request->getStr('ssh-keyfile'));
 
       $repository->setDetail('http-login', $request->getStr('http-login'));
       $repository->setDetail('http-pass',  $request->getStr('http-pass'));
 
       if ($repository->getDetail('ssh-key') &&
           $repository->getDetail('ssh-keyfile')) {
         $errors[] =
           "Specify only one of 'SSH Private Key' and 'SSH Private Key File', ".
           "not both.";
         $e_ssh_key = 'Choose Only One';
         $e_ssh_keyfile = 'Choose Only One';
       }
 
       $repository->setDetail(
         'herald-disabled',
         $request->getInt('herald-disabled', 0));
 
       if ($is_svn) {
         $repository->setUUID($request->getStr('uuid'));
         $subpath = ltrim($request->getStr('svn-subpath'), '/');
         if ($subpath) {
           $subpath = rtrim($subpath, '/').'/';
         }
         $repository->setDetail('svn-subpath', $subpath);
       }
 
       $repository->setDetail(
         'detail-parser',
         $request->getStr(
           'detail-parser',
           'PhabricatorRepositoryDefaultCommitMessageDetailParser'));
 
       if ($tracking) {
         if (!$repository->getDetail('remote-uri')) {
           $e_uri = 'Required';
           $errors[] = "Repository URI is required.";
         } else if ($is_svn &&
           !preg_match('@/$@', $repository->getDetail('remote-uri'))) {
 
           $e_uri = 'Invalid';
           $errors[] = 'Subversion Repository Root must end in a slash ("/").';
         } else {
           $e_uri = null;
         }
 
         if ($has_local) {
           if (!$repository->getDetail('local-path')) {
             $e_path = 'Required';
             $errors[] = "Local path is required.";
           } else {
             $e_path = null;
           }
         }
       }
 
       if (!$errors) {
         $repository->save();
         return id(new AphrontRedirectResponse())
           ->setURI('/repository/edit/'.$repository_id.'/tracking/?saved=true');
       }
     }
 
     $error_view = null;
     if ($errors) {
       $error_view = new AphrontErrorView();
       $error_view->setErrors($errors);
       $error_view->setTitle('Form Errors');
     } else if ($request->getStr('saved')) {
       $error_view = new AphrontErrorView();
       $error_view->setSeverity(AphrontErrorView::SEVERITY_NOTICE);
       $error_view->setTitle('Changes Saved');
       $error_view->appendChild('Tracking changes were saved.');
     } else if (!$repository->isTracked()) {
       $error_view = new AphrontErrorView();
       $error_view->setSeverity(AphrontErrorView::SEVERITY_WARNING);
       $error_view->setTitle('Repository Not Tracked');
       $error_view->appendChild(
         'Tracking is currently "Disabled" for this repository, so it will '.
         'not be imported into Phabricator. You can enable it below.');
     }
 
     switch ($repository->getVersionControlSystem()) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         $is_git = true;
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         $is_svn = true;
         break;
     }
 
     $doc_href = PhabricatorEnv::getDoclink('article/Diffusion_User_Guide.html');
     $user_guide_link = phutil_render_tag(
       'a',
       array(
         'href' => $doc_href,
       ),
       'Diffusion User Guide');
 
     $form = new AphrontFormView();
     $form
       ->setUser($user)
       ->setAction('/repository/edit/'.$repository->getID().'/tracking/')
       ->appendChild(
         '<p class="aphront-form-instructions">Phabricator can track '.
         'repositories, importing commits as they happen and notifying '.
         'Differential, Diffusion, Herald, and other services. To enable '.
         'tracking for a repository, configure it here and then start (or '.
         'restart) the daemons. More information is available in the '.
         '<strong>'.$user_guide_link.'</strong>.</p>');
 
     $form
       ->appendChild(
         id(new AphrontFormInsetView())
           ->setTitle('Basics')
           ->appendChild(id(new AphrontFormStaticControl())
             ->setLabel('Repository Name')
             ->setValue($repository->getName()))
           ->appendChild(id(new AphrontFormSelectControl())
             ->setName('tracking')
             ->setLabel('Tracking')
             ->setOptions(array(
                 'disabled'  => 'Disabled',
                 'enabled'   => 'Enabled',
                 ))
             ->setValue(
               $repository->isTracked()
               ? 'enabled'
               : 'disabled')));
 
     $inset = new AphrontFormInsetView();
     $inset->setTitle('Remote URI');
 
     $clone_command = null;
     $fetch_command = null;
     if ($is_git) {
       $clone_command = 'git clone';
       $fetch_command = 'git fetch';
     } else if ($is_mercurial) {
       $clone_command = 'hg clone';
       $fetch_command = 'hg pull';
     }
 
     $uri_label = 'Repository URI';
     if ($has_local) {
       if ($is_git) {
         $instructions =
           'Enter the URI to clone this repository from. It should look like '.
           '<tt>git@github.com:example/example.git</tt>, '.
           '<tt>ssh://user@host.com/git/example.git</tt>, or '.
           '<tt>file:///local/path/to/repo</tt>';
       } else if ($is_mercurial) {
         $instructions =
           'Enter the URI to clone this repository from. It should look '.
           'something like <tt>ssh://user@host.com/hg/example</tt>';
       }
       $inset->appendChild(
         '<p class="aphront-form-instructions">'.$instructions.'</p>');
     } else if ($is_svn) {
       $instructions =
         'Enter the <strong>Repository Root</strong> for this SVN repository. '.
         'You can figure this out by running <tt>svn info</tt> and looking at '.
         'the value in the <tt>Repository Root</tt> field. It should be a URI '.
         'and look like <tt>http://svn.example.org/svn/</tt>, '.
         '<tt>svn+ssh://svn.example.com/svnroot/</tt>, or '.
         '<tt>svn://svn.example.net/svn/</tt>';
       $inset->appendChild(
         '<p class="aphront-form-instructions">'.$instructions.'</p>');
       $uri_label = 'Repository Root';
     }
 
     $inset
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('uri')
           ->setLabel($uri_label)
           ->setID('remote-uri')
           ->setValue($repository->getDetail('remote-uri'))
           ->setError($e_uri));
 
     $inset->appendChild(
       '<div class="aphront-form-instructions">'.
         'If you want to connect to this repository over SSH, enter the '.
         'username and private key to use. You can leave these fields blank if '.
         'the repository does not use SSH.'.
       '</div>');
 
     $inset
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('ssh-login')
           ->setLabel('SSH User')
           ->setValue($repository->getDetail('ssh-login')))
       ->appendChild(
         id(new AphrontFormTextAreaControl())
           ->setName('ssh-key')
           ->setLabel('SSH Private Key')
           ->setHeight(AphrontFormTextAreaControl::HEIGHT_VERY_SHORT)
           ->setValue($repository->getDetail('ssh-key'))
           ->setError($e_ssh_key)
           ->setCaption('Specify the entire private key, <em>or</em>...'))
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('ssh-keyfile')
           ->setLabel('SSH Private Key File')
           ->setValue($repository->getDetail('ssh-keyfile'))
           ->setError($e_ssh_keyfile)
           ->setCaption(
             '...specify a path on disk where the daemon should '.
             'look for a private key.'));
 
     if ($has_auth_support) {
       $inset
         ->appendChild(
           '<div class="aphront-form-instructions">'.
             'If you want to connect to this repository with a username and '.
             'password, such as over HTTP Basic Auth or SVN with SASL, '.
             'enter the username and password to use. You can leave these '.
             'fields blank if the repository does not use a username and '.
             'password for authentication.'.
           '</div>')
         ->appendChild(
           id(new AphrontFormTextControl())
             ->setName('http-login')
             ->setLabel('Username')
             ->setValue($repository->getDetail('http-login')))
         ->appendChild(
           id(new AphrontFormPasswordControl())
             ->setName('http-pass')
             ->setLabel('Password')
             ->setValue($repository->getDetail('http-pass')));
     }
 
     $inset
       ->appendChild(
         '<div class="aphront-form-important">'.
           'To test your authentication configuration, <strong>save this '.
           'form</strong> and then run this script:'.
           '<code>'.
             'phabricator/ $ ./scripts/repository/test_connection.php '.
             phutil_escape_html($repository->getCallsign()).
           '</code>'.
           'This will verify that your configuration is correct and the '.
           'daemons can connect to the remote repository and pull changes '.
           'from it.'.
         '</div>');
 
     $form->appendChild($inset);
 
     $inset = new AphrontFormInsetView();
     $inset->setTitle('Repository Information');
 
     if ($has_local) {
       $inset->appendChild(
         '<p class="aphront-form-instructions">Select a path on local disk '.
         'which the daemons should <tt>'.$clone_command.'</tt> the repository '.
         'into. This must be readable and writable by the daemons, and '.
         'readable by the webserver. The daemons will <tt>'.$fetch_command.
         '</tt> and keep this repository up to date.</p>');
       $inset->appendChild(
         id(new AphrontFormTextControl())
           ->setName('path')
           ->setLabel('Local Path')
           ->setValue($repository->getDetail('local-path'))
           ->setError($e_path));
     } else if ($is_svn) {
       $inset->appendChild(
         '<p class="aphront-form-instructions">If you only want to parse one '.
         'subpath of the repository, specify it here, relative to the '.
         'repository root (e.g., <tt>trunk/</tt> or <tt>projects/wheel/</tt>). '.
         'If you want to parse multiple subdirectories, create a separate '.
         'Phabricator repository for each one.</p>');
       $inset->appendChild(
         id(new AphrontFormTextControl())
           ->setName('svn-subpath')
           ->setLabel('Subpath')
           ->setValue($repository->getDetail('svn-subpath'))
           ->setError($e_path));
     }
 
     if ($has_branch_filter) {
       $branch_filter_str = implode(
         ', ',
         array_keys($repository->getDetail('branch-filter', array())));
       $inset
         ->appendChild(
           id(new AphrontFormTextControl())
             ->setName('branch-filter')
             ->setLabel('Track Only')
             ->setValue($branch_filter_str)
             ->setCaption(
               'Optional list of branches to track. Other branches will be '.
               'completely ignored. If left empty, all branches are tracked. '.
               'Example: <tt>master, release</tt>'));
     }
 
     $inset
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('frequency')
           ->setLabel('Pull Frequency')
           ->setValue($repository->getDetail('pull-frequency', 15))
           ->setCaption(
             'Number of seconds daemon should sleep between requests. Larger '.
             'numbers reduce load but also decrease responsiveness.'));
 
     $form->appendChild($inset);
 
     $inset = new AphrontFormInsetView();
     $inset->setTitle('Application Configuration');
 
     if ($has_branches) {
       $inset
         ->appendChild(
           id(new AphrontFormTextControl())
             ->setName('default-branch')
             ->setLabel('Default Branch')
             ->setValue($repository->getDefaultBranch())
             ->setError($e_branch)
             ->setCaption(
               'Default branch to show in Diffusion.'));
     }
 
     $inset
       ->appendChild(id(new AphrontFormSelectControl())
         ->setName('autoclose')
         ->setLabel('Autoclose')
         ->setOptions(array(
             'enabled'   => 'Enabled: Automatically Close Pushed Revisions',
             'disabled'  => 'Disabled: Ignore Pushed Revisions',
             ))
         ->setCaption(
-          "Automatically close Differential revisions which are pushed to ".
-          "this repository.")
+          "Automatically close Differential revisions when associated commits ".
+          "are pushed to this repository.")
         ->setValue(
           $repository->getDetail('disable-autoclose', false)
           ? 'disabled'
           : 'enabled'));
 
+    if ($has_branch_filter) {
+      $close_commits_filter_str = implode(
+          ', ',
+          array_keys($repository->getDetail('close-commits-filter', array())));
+      $inset
+        ->appendChild(
+          id(new AphrontFormTextControl())
+            ->setName('close-commits-filter')
+            ->setLabel('Autoclose Branches')
+            ->setValue($close_commits_filter_str)
+            ->setCaption(
+              'Optional list of branches which can trigger autoclose. '.
+              'If left empty, all branches trigger autoclose.'));
+    }
+
     $inset
       ->appendChild(
         id(new AphrontFormTextControl())
           ->setName('default-owners-path')
           ->setLabel('Default Owners Path')
           ->setValue(
             $repository->getDetail(
               'default-owners-path',
               '/'))
           ->setCaption('Default path in Owners tool.'));
 
     $inset
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setName('herald-disabled')
           ->setLabel('Herald Enabled')
           ->setValue($repository->getDetail('herald-disabled', 0))
           ->setOptions(
             array(
               0 => 'Enabled - Send Email',
               1 => 'Disabled - Do Not Send Email',
             ))
           ->setCaption(
             'You can temporarily disable Herald commit notifications when '.
             'reparsing a repository or importing a new repository.'));
 
     $parsers = id(new PhutilSymbolLoader())
       ->setAncestorClass('PhabricatorRepositoryCommitMessageDetailParser')
       ->selectSymbolsWithoutLoading();
     $parsers = ipull($parsers, 'name', 'name');
 
     $inset
       ->appendChild(
         '<p class="aphront-form-instructions">If you extend the commit '.
         'message format, you can provide a new parser which will extract '.
         'extra information from it when commits are imported. This is an '.
         'advanced feature, and using the default parser will be suitable '.
         'in most cases.</p>')
       ->appendChild(
         id(new AphrontFormSelectControl())
           ->setName('detail-parser')
           ->setLabel('Detail Parser')
           ->setOptions($parsers)
           ->setValue(
             $repository->getDetail(
               'detail-parser',
               'PhabricatorRepositoryDefaultCommitMessageDetailParser')));
 
     if ($is_svn) {
       $inset
         ->appendChild(
           id(new AphrontFormTextControl())
             ->setName('uuid')
             ->setLabel('UUID')
             ->setValue($repository->getUUID())
             ->setCaption('Repository UUID from <tt>svn info</tt>.'));
     }
 
     $form->appendChild($inset);
 
     $form
       ->appendChild(
         id(new AphrontFormSubmitControl())
           ->setValue('Save Configuration'));
 
     $panel = new AphrontPanelView();
     $panel->setHeader('Repository Tracking');
     $panel->appendChild($form);
     $panel->setWidth(AphrontPanelView::WIDTH_WIDE);
 
     $nav = $this->sideNav;
     $nav->appendChild($error_view);
     $nav->appendChild($panel);
 
     return $this->buildStandardPageResponse(
       $nav,
       array(
         'title' => 'Edit Repository Tracking',
       ));
   }
 
 }
diff --git a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
index 213f90cadd..f540258f6a 100644
--- a/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
+++ b/src/applications/repository/daemon/PhabricatorRepositoryPullLocalDaemon.php
@@ -1,800 +1,885 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 /**
  * Run pull commands on local working copies to keep them up to date. This
  * daemon handles all repository types.
  *
  * By default, the daemon pulls **every** repository. If you want it to be
  * responsible for only some repositories, you can launch it with a list of
  * PHIDs or callsigns:
  *
  *   ./phd launch repositorypulllocal -- X Q Z
  *
  * You can also launch a daemon which is responsible for all //but// one or
  * more repositories:
  *
  *   ./phd launch repositorypulllocal -- --not A --not B
  *
  * If you have a very large number of repositories and some aren't being pulled
  * as frequently as you'd like, you can either change the pull frequency of
  * the less-important repositories to a larger number (so the daemon will skip
  * them more often) or launch one daemon for all the less-important repositories
  * and one for the more important repositories (or one for each more important
  * repository).
  *
  * @task pull   Pulling Repositories
  * @task git    Git Implementation
  * @task hg     Mercurial Implementation
  */
 final class PhabricatorRepositoryPullLocalDaemon
   extends PhabricatorDaemon {
 
   private static $commitCache = array();
 
 
 /* -(  Pulling Repositories  )----------------------------------------------- */
 
 
   /**
    * @task pull
    */
   public function run() {
     $argv = $this->getArgv();
     array_unshift($argv, __CLASS__);
     $args = new PhutilArgumentParser($argv);
     $args->parse(
       array(
         array(
           'name'      => 'no-discovery',
           'help'      => 'Pull only, without discovering commits.',
         ),
         array(
           'name'      => 'not',
           'param'     => 'repository',
           'repeat'    => true,
           'help'      => 'Do not pull __repository__.',
         ),
         array(
           'name'      => 'repositories',
           'wildcard'  => true,
           'help'      => 'Pull specific __repositories__ instead of all.',
         ),
       ));
 
     $no_discovery   = $args->getArg('no-discovery');
     $repo_names     = $args->getArg('repositories');
     $exclude_names  = $args->getArg('not');
 
     // Each repository has an individual pull frequency; after we pull it,
     // wait that long to pull it again. When we start up, try to pull everything
     // serially.
     $retry_after = array();
 
     $min_sleep = 15;
 
     while (true) {
       $repositories = $this->loadRepositories($repo_names);
       if ($exclude_names) {
         $exclude = $this->loadRepositories($exclude_names);
         $repositories = array_diff_key($repositories, $exclude);
       }
 
       // Shuffle the repositories, then re-key the array since shuffle()
       // discards keys. This is mostly for startup, we'll use soft priorities
       // later.
       shuffle($repositories);
       $repositories = mpull($repositories, null, 'getID');
 
       // If any repositories were deleted, remove them from the retry timer map
       // so we don't end up with a retry timer that never gets updated and
       // causes us to sleep for the minimum amount of time.
       $retry_after = array_select_keys(
         $retry_after,
         array_keys($repositories));
 
       // Assign soft priorities to repositories based on how frequently they
       // should pull again.
       asort($retry_after);
       $repositories = array_select_keys(
         $repositories,
         array_keys($retry_after)) + $repositories;
 
       foreach ($repositories as $id => $repository) {
         $after = idx($retry_after, $id, 0);
         if ($after > time()) {
           continue;
         }
 
         $tracked = $repository->isTracked();
         if (!$tracked) {
           continue;
         }
 
         try {
           self::pullRepository($repository);
 
           if (!$no_discovery) {
             // TODO: It would be nice to discover only if we pulled something,
             // but this isn't totally trivial.
             self::discoverRepository($repository);
           }
 
           $sleep_for = $repository->getDetail('pull-frequency', $min_sleep);
           $retry_after[$id] = time() + $sleep_for;
         } catch (Exception $ex) {
           $retry_after[$id] = time() + $min_sleep;
           phlog($ex);
         }
 
         $this->stillWorking();
       }
 
       if ($retry_after) {
         $sleep_until = max(min($retry_after), time() + $min_sleep);
       } else {
         $sleep_until = time() + $min_sleep;
       }
 
       $this->sleep($sleep_until - time());
     }
   }
 
 
   /**
    * @task pull
    */
   protected function loadRepositories(array $names) {
     if (!count($names)) {
       return id(new PhabricatorRepository())->loadAll();
     } else {
       return PhabricatorRepository::loadAllByPHIDOrCallsign($names);
     }
   }
 
 
   /**
    * @task pull
    */
   public static function pullRepository(PhabricatorRepository $repository) {
     $vcs = $repository->getVersionControlSystem();
 
     $is_svn = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_SVN);
     $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
     $is_hg = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL);
 
     if ($is_svn) {
       return;
     }
 
     $callsign = $repository->getCallsign();
 
     if (!$is_git && !$is_hg) {
       throw new Exception(
         "Unknown VCS '{$vcs}' for repository '{$callsign}'!");
     }
 
     $local_path = $repository->getDetail('local-path');
     if (!$local_path) {
       throw new Exception(
         "No local path is available for repository '{$callsign}'.");
     }
 
     if (!Filesystem::pathExists($local_path)) {
       $dirname = dirname($local_path);
       if (!Filesystem::pathExists($dirname)) {
-        echo "Creating new directory '{$dirname}' ".
-             "for repository '{$callsign}'.\n";
         Filesystem::createDirectory($dirname, 0755, $recursive = true);
       }
 
       if ($is_git) {
         return self::executeGitCreate($repository, $local_path);
       } else if ($is_hg) {
         return self::executeHgCreate($repository, $local_path);
       }
     } else {
       if ($is_git) {
         return self::executeGitUpdate($repository, $local_path);
       } else if ($is_hg) {
         return self::executeHgUpdate($repository, $local_path);
       }
     }
   }
 
   public static function discoverRepository(PhabricatorRepository $repository) {
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         return self::executeGitDiscover($repository);
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         return self::executeSvnDiscover($repository);
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         return self::executeHgDiscover($repository);
       default:
         throw new Exception("Unknown VCS '{$vcs}'!");
     }
   }
 
 
   private static function isKnownCommit(
     PhabricatorRepository $repository,
     $target) {
 
     if (self::getCache($repository, $target)) {
       return true;
     }
 
     $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
       'repositoryID = %s AND commitIdentifier = %s',
       $repository->getID(),
       $target);
 
     if (!$commit) {
       return false;
     }
 
     self::setCache($repository, $target);
     while (count(self::$commitCache) > 2048) {
       array_shift(self::$commitCache);
     }
 
     return true;
   }
 
+  private static function isKnownCommitOnBranch(
+    PhabricatorRepository $repository,
+    $target,
+    $branch,
+    $any_autoclose_branch = false) {
+
+    $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
+      'repositoryID = %s AND commitIdentifier = %s',
+      $repository->getID(),
+      $target);
+
+    $data = $commit->loadCommitData();
+    if (!$data) {
+      return false;
+    }
+
+    if ($any_autoclose_branch) {
+      if ($repository->shouldAutocloseCommit($commit, $data)) {
+        return true;
+      }
+    }
+
+    $branches = $data->getCommitDetail('seenOnBranches', array());
+    if (in_array($branch, $branches)) {
+      return true;
+    }
+
+    return false;
+  }
+
   private static function recordCommit(
     PhabricatorRepository $repository,
     $commit_identifier,
     $epoch) {
 
     $commit = new PhabricatorRepositoryCommit();
     $commit->setRepositoryID($repository->getID());
     $commit->setCommitIdentifier($commit_identifier);
     $commit->setEpoch($epoch);
 
     try {
       $commit->save();
 
       $event = new PhabricatorTimelineEvent(
         'cmit',
         array(
           'id' => $commit->getID(),
         ));
       $event->recordEvent();
 
       self::insertTask($repository, $commit);
 
       queryfx(
         $repository->establishConnection('w'),
         'INSERT INTO %T (repositoryID, size, lastCommitID, epoch)
           VALUES (%d, 1, %d, %d)
           ON DUPLICATE KEY UPDATE
             size = size + 1,
             lastCommitID =
               IF(VALUES(epoch) > epoch, VALUES(lastCommitID), lastCommitID),
             epoch = IF(VALUES(epoch) > epoch, VALUES(epoch), epoch)',
         PhabricatorRepository::TABLE_SUMMARY,
         $repository->getID(),
         $commit->getID(),
         $epoch);
 
       self::setCache($repository, $commit_identifier);
     } catch (AphrontQueryDuplicateKeyException $ex) {
       // Ignore. This can happen because we discover the same new commit
       // more than once when looking at history, or because of races or
       // data inconsistency or cosmic radiation; in any case, we're still
       // in a good state if we ignore the failure.
       self::setCache($repository, $commit_identifier);
     }
   }
 
+  private static function updateCommit(
+    PhabricatorRepository $repository,
+    $commit_identifier,
+    $branch) {
+
+    $commit = id(new PhabricatorRepositoryCommit())->loadOneWhere(
+      'repositoryID = %s AND commitIdentifier = %s',
+      $repository->getID(),
+      $commit_identifier);
+
+    $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
+      'commitID = %d',
+      $commit->getID());
+    if (!$data) {
+      $data = new PhabricatorRepositoryCommitData();
+      $data->setCommitID($commit->getID());
+    }
+    $branches = $data->getCommitDetail('seenOnBranches', array());
+    $branches[] = $branch;
+    $data->setCommitDetail('seenOnBranches', $branches);
+    $data->save();
+
+    self::insertTask(
+      $repository,
+      $commit,
+      array(
+        'only' => true
+      ));
+  }
+
   private static function insertTask(
     PhabricatorRepository $repository,
-    PhabricatorRepositoryCommit $commit) {
+    PhabricatorRepositoryCommit $commit,
+    $data = array()) {
 
     $vcs = $repository->getVersionControlSystem();
     switch ($vcs) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         $class = 'PhabricatorRepositoryGitCommitMessageParserWorker';
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         $class = 'PhabricatorRepositorySvnCommitMessageParserWorker';
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $class = 'PhabricatorRepositoryMercurialCommitMessageParserWorker';
         break;
       default:
         throw new Exception("Unknown repository type '{$vcs}'!");
     }
 
     $task = new PhabricatorWorkerTask();
     $task->setTaskClass($class);
-    $task->setData(
-      array(
-        'commitID' => $commit->getID(),
-      ));
+    $data['commitID'] = $commit->getID();
+
+    $task->setData($data);
     $task->save();
   }
 
 
   private static function setCache(
     PhabricatorRepository $repository,
     $commit_identifier) {
 
     $key = self::getCacheKey($repository, $commit_identifier);
     self::$commitCache[$key] = true;
   }
 
   private static function getCache(
     PhabricatorRepository $repository,
     $commit_identifier) {
 
     $key = self::getCacheKey($repository, $commit_identifier);
     return idx(self::$commitCache, $key, false);
   }
 
   private static function getCacheKey(
     PhabricatorRepository $repository,
     $commit_identifier) {
 
     return $repository->getID().':'.$commit_identifier;
   }
 
 
 
 /* -(  Git Implementation  )------------------------------------------------- */
 
 
   /**
    * @task git
    */
   private static function executeGitCreate(
     PhabricatorRepository $repository,
     $path) {
 
     $repository->execxRemoteCommand(
       'clone --origin origin %s %s',
       $repository->getRemoteURI(),
       rtrim($path, '/'));
   }
 
 
   /**
    * @task git
    */
   private static function executeGitUpdate(
     PhabricatorRepository $repository,
     $path) {
 
     // Run a bunch of sanity checks to detect people checking out repositories
     // inside other repositories, making empty directories, pointing the local
     // path at some random file or path, etc.
 
     list($err, $stdout) = $repository->execLocalCommand(
       'rev-parse --show-toplevel');
 
     if ($err) {
 
       // Try to raise a more tailored error message in the more common case
       // of the user creating an empty directory. (We could try to remove it,
       // but might not be able to, and it's much simpler to raise a good
       // message than try to navigate those waters.)
       if (is_dir($path)) {
         $files = Filesystem::listDirectory($path, $include_hidden = true);
         if (!$files) {
           throw new Exception(
             "Expected to find a git repository at '{$path}', but there ".
             "is an empty directory there. Remove the directory: the daemon ".
             "will run 'git clone' for you.");
         }
       }
 
       throw new Exception(
         "Expected to find a git repository at '{$path}', but there is ".
         "a non-repository directory (with other stuff in it) there. Move or ".
         "remove this directory (or reconfigure the repository to use a ".
         "different directory), and then either clone a repository yourself ".
         "or let the daemon do it.");
     } else {
       $repo_path = rtrim($stdout, "\n");
 
       if (empty($repo_path)) {
         throw new Exception(
           "Expected to find a git repository at '{$path}', but ".
           "there was no result from `git rev-parse --show-toplevel`. ".
           "Something is misconfigured or broken. The git repository ".
           "may be inside a '.git/' directory.");
       }
 
       if (!Filesystem::pathsAreEquivalent($repo_path, $path)) {
         throw new Exception(
           "Expected to find repo at '{$path}', but the actual ".
           "git repository root for this directory is '{$repo_path}'. ".
           "Something is misconfigured. The repository's 'Local Path' should ".
           "be set to some place where the daemon can check out a working ".
           "copy, and should not be inside another git repository.");
       }
     }
 
 
     // This is a local command, but needs credentials.
     $future = $repository->getRemoteCommandFuture('fetch --all --prune');
     $future->setCWD($path);
     $future->resolvex();
   }
 
 
   /**
    * @task git
    */
   private static function executeGitDiscover(
     PhabricatorRepository $repository) {
 
     list($remotes) = $repository->execxLocalCommand(
       'remote show -n origin');
 
     $matches = null;
     if (!preg_match('/^\s*Fetch URL:\s*(.*?)\s*$/m', $remotes, $matches)) {
       throw new Exception(
         "Expected 'Fetch URL' in 'git remote show -n origin'.");
     }
 
     self::executeGitverifySameOrigin(
       $matches[1],
       $repository->getRemoteURI(),
       $repository->getLocalPath());
 
     list($stdout) = $repository->execxLocalCommand(
       'branch -r --verbose --no-abbrev');
 
     $branches = DiffusionGitBranchQuery::parseGitRemoteBranchOutput(
       $stdout,
       $only_this_remote = DiffusionBranchInformation::DEFAULT_GIT_REMOTE);
 
     $tracked_something = false;
+    $found_something = false;
     foreach ($branches as $name => $commit) {
       if (!$repository->shouldTrackBranch($name)) {
         continue;
       }
 
       $tracked_something = true;
 
       if (self::isKnownCommit($repository, $commit)) {
         continue;
       } else {
         self::executeGitDiscoverCommit($repository, $commit);
       }
     }
 
     if (!$tracked_something) {
       $repo_name = $repository->getName();
       $repo_callsign = $repository->getCallsign();
       throw new Exception(
         "Repository r{$repo_callsign} '{$repo_name}' has no tracked branches! ".
         "Verify that your branch filtering settings are correct.");
     }
+
+    foreach ($branches as $name => $commit) {
+      if (!$repository->shouldTrackBranch($name)) {
+        continue;
+      }
+
+      if (!$repository->shouldAutocloseBranch($name)) {
+        continue;
+      }
+
+      if (self::isKnownCommitOnBranch($repository, $commit, $name, true)) {
+        continue;
+      }
+
+      self::executeGitDiscoverCommit($repository, $commit, $name);
+    }
   }
 
 
   /**
    * @task git
    */
   private static function executeGitDiscoverCommit(
     PhabricatorRepository $repository,
-    $commit) {
+    $commit,
+    $branch = null) {
 
     $discover = array($commit);
     $insert = array($commit);
 
     $seen_parent = array();
 
     while (true) {
       $target = array_pop($discover);
       list($parents) = $repository->execxLocalCommand(
         'log -n1 --pretty="%%P" %s',
         $target);
       $parents = array_filter(explode(' ', trim($parents)));
       foreach ($parents as $parent) {
         if (isset($seen_parent[$parent])) {
           // We end up in a loop here somehow when we parse Arcanist if we
           // don't do this. TODO: Figure out why and draw a pretty diagram
           // since it's not evident how parsing a DAG with this causes the
           // loop to stop terminating.
           continue;
         }
         $seen_parent[$parent] = true;
-        if (!self::isKnownCommit($repository, $parent)) {
+        if ($branch !== null) {
+          $known = self::isKnownCommitOnBranch($repository, $parent, $branch);
+        } else {
+          $known = self::isKnownCommit($repository, $parent);
+        }
+        if (!$known) {
           $discover[] = $parent;
           $insert[] = $parent;
         }
       }
       if (empty($discover)) {
         break;
       }
     }
 
     while (true) {
       $target = array_pop($insert);
       list($epoch) = $repository->execxLocalCommand(
         'log -n1 --pretty="%%ct" %s',
         $target);
       $epoch = trim($epoch);
 
-      self::recordCommit($repository, $target, $epoch);
+      if ($branch !== null) {
+        self::updateCommit($repository, $target, $branch);
+      } else {
+        self::recordCommit($repository, $target, $epoch);
+      }
 
       if (empty($insert)) {
         break;
       }
     }
   }
 
 
   /**
    * @task git
    */
   public static function executeGitVerifySameOrigin($remote, $expect, $where) {
     $remote_uri = PhabricatorRepository::newPhutilURIFromGitURI($remote);
     $expect_uri = PhabricatorRepository::newPhutilURIFromGitURI($expect);
 
     $remote_path = $remote_uri->getPath();
     $expect_path = $expect_uri->getPath();
 
     $remote_match = self::executeGitNormalizePath($remote_path);
     $expect_match = self::executeGitNormalizePath($expect_path);
 
     if ($remote_match != $expect_match) {
       throw new Exception(
         "Working copy at '{$where}' has a mismatched origin URL. It has ".
         "origin URL '{$remote}' (with remote path '{$remote_path}'), but the ".
         "configured URL '{$expect}' (with remote path '{$expect_path}') is ".
         "expected. Refusing to proceed because this may indicate that the ".
         "working copy is actually some other repository.");
     }
   }
 
 
   /**
    * @task git
    */
   private static function executeGitNormalizePath($path) {
     // Strip away trailing "/" and ".git", so similar paths correctly match.
 
     $path = rtrim($path, '/');
     $path = preg_replace('/\.git$/', '', $path);
     return $path;
   }
 
 
 /* -(  Mercurial Implementation  )------------------------------------------- */
 
 
   /**
    * @task hg
    */
   private static function executeHgCreate(
     PhabricatorRepository $repository,
     $path) {
 
     $repository->execxRemoteCommand(
       'clone %s %s',
       $repository->getRemoteURI(),
       rtrim($path, '/'));
   }
 
 
   /**
    * @task hg
    */
   private static function executeHgUpdate(
     PhabricatorRepository $repository,
     $path) {
 
     // This is a local command, but needs credentials.
     $future = $repository->getRemoteCommandFuture('pull -u');
     $future->setCWD($path);
 
     try {
       $future->resolvex();
     } catch (CommandException $ex) {
       $err = $ex->getError();
       $stdout = $ex->getStdOut();
 
       // NOTE: Between versions 2.1 and 2.1.1, Mercurial changed the behavior
       // of "hg pull" to return 1 in case of a successful pull with no changes.
       // This behavior has been reverted, but users who updated between Feb 1,
       // 2012 and Mar 1, 2012 will have the erroring version. Do a dumb test
       // against stdout to check for this possibility.
       // See: https://github.com/facebook/phabricator/issues/101/
 
       // NOTE: Mercurial has translated versions, which translate this error
       // string. In a translated version, the string will be something else,
       // like "aucun changement trouve". There didn't seem to be an easy way
       // to handle this (there are hard ways but this is not a common problem
       // and only creates log spam, not application failures). Assume English.
 
       // TODO: Remove this once we're far enough in the future that deployment
       // of 2.1 is exceedingly rare?
       if ($err == 1 && preg_match('/no changes found/', $stdout)) {
         return;
       } else {
         throw $ex;
       }
     }
   }
 
   private static function executeHgDiscover(PhabricatorRepository $repository) {
     // NOTE: "--debug" gives us 40-character hashes.
     list($stdout) = $repository->execxLocalCommand('--debug branches');
 
     $branches = ArcanistMercurialParser::parseMercurialBranches($stdout);
     $got_something = false;
     foreach ($branches as $name => $branch) {
       $commit = $branch['rev'];
       if (self::isKnownCommit($repository, $commit)) {
         continue;
       } else {
         self::executeHgDiscoverCommit($repository, $commit);
         $got_something = true;
       }
     }
 
     return $got_something;
   }
 
   private static function executeHgDiscoverCommit(
     PhabricatorRepository $repository,
     $commit) {
 
     $discover = array($commit);
     $insert = array($commit);
 
     $seen_parent = array();
 
     $stream = new PhabricatorMercurialGraphStream($repository);
 
     // For all the new commits at the branch heads, walk backward until we
     // find only commits we've aleady seen.
     while ($discover) {
       $target = array_pop($discover);
 
       $parents = $stream->getParents($target);
 
       foreach ($parents as $parent) {
         if (isset($seen_parent[$parent])) {
           continue;
         }
         $seen_parent[$parent] = true;
         if (!self::isKnownCommit($repository, $parent)) {
           $discover[] = $parent;
           $insert[] = $parent;
         }
       }
     }
 
     foreach ($insert as $target) {
       $epoch = $stream->getCommitDate($target);
       self::recordCommit($repository, $target, $epoch);
     }
   }
 
 
 /* -(  Subversion Implementation  )------------------------------------------ */
 
 
   private static function executeSvnDiscover(
     PhabricatorRepository $repository) {
 
     $uri = self::executeSvnGetBaseSVNLogURI($repository);
 
     list($xml) = $repository->execxRemoteCommand(
       'log --xml --quiet --limit 1 %s@HEAD',
       $uri);
 
     $results = self::executeSvnParseLogXML($xml);
     $commit = head_key($results);
     $epoch  = head($results);
 
     if (self::isKnownCommit($repository, $commit)) {
       return false;
     }
 
     self::executeSvnDiscoverCommit($repository, $commit, $epoch);
     return true;
   }
 
   private static function executeSvnDiscoverCommit(
     PhabricatorRepository $repository,
     $commit,
     $epoch) {
 
     $uri = self::executeSvnGetBaseSVNLogURI($repository);
 
     $discover = array(
       $commit => $epoch,
     );
     $upper_bound = $commit;
 
     $limit = 1;
     while ($upper_bound > 1 &&
            !self::isKnownCommit($repository, $upper_bound)) {
       // Find all the unknown commits on this path. Note that we permit
       // importing an SVN subdirectory rather than the entire repository, so
       // commits may be nonsequential.
       list($err, $xml, $stderr) = $repository->execRemoteCommand(
         ' log --xml --quiet --limit %d %s@%d',
         $limit,
         $uri,
         $upper_bound - 1);
       if ($err) {
         if (preg_match('/(path|File) not found/', $stderr)) {
           // We've gone all the way back through history and this path was not
           // affected by earlier commits.
           break;
         } else {
           throw new Exception("svn log error #{$err}: {$stderr}");
         }
       }
       $discover += self::executeSvnParseLogXML($xml);
 
       $upper_bound = min(array_keys($discover));
 
       // Discover 2, 4, 8, ... 256 logs at a time. This allows us to initially
       // import large repositories fairly quickly, while pulling only as much
       // data as we need in the common case (when we've already imported the
       // repository and are just grabbing one commit at a time).
       $limit = min($limit * 2, 256);
     }
 
     // NOTE: We do writes only after discovering all the commits so that we're
     // never left in a state where we've missed commits -- if the discovery
     // script terminates it can always resume and restore the import to a good
     // state. This is also why we sort the discovered commits so we can do
     // writes forward from the smallest one.
 
     ksort($discover);
     foreach ($discover as $commit => $epoch) {
       self::recordCommit($repository, $commit, $epoch);
     }
   }
 
   private static function executeSvnParseLogXML($xml) {
     $xml = phutil_utf8ize($xml);
 
     $result = array();
 
     $log = new SimpleXMLElement($xml);
     foreach ($log->logentry as $entry) {
       $commit = (int)$entry['revision'];
       $epoch  = (int)strtotime((string)$entry->date[0]);
       $result[$commit] = $epoch;
     }
 
     return $result;
   }
 
 
   private static function executeSvnGetBaseSVNLogURI(
     PhabricatorRepository $repository) {
 
     $uri = $repository->getDetail('remote-uri');
     $subpath = $repository->getDetail('svn-subpath');
 
     return $uri.$subpath;
   }
 
 }
diff --git a/src/applications/repository/storage/PhabricatorRepository.php b/src/applications/repository/storage/PhabricatorRepository.php
index 7af5130d01..745438e086 100644
--- a/src/applications/repository/storage/PhabricatorRepository.php
+++ b/src/applications/repository/storage/PhabricatorRepository.php
@@ -1,449 +1,475 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 final class PhabricatorRepository extends PhabricatorRepositoryDAO {
 
   const TABLE_PATH = 'repository_path';
   const TABLE_PATHCHANGE = 'repository_pathchange';
   const TABLE_FILESYSTEM = 'repository_filesystem';
   const TABLE_SUMMARY = 'repository_summary';
   const TABLE_BADCOMMIT = 'repository_badcommit';
 
   protected $phid;
   protected $name;
   protected $callsign;
   protected $uuid;
 
   protected $versionControlSystem;
   protected $details = array();
 
   private $sshKeyfile;
 
   public function getConfiguration() {
     return array(
       self::CONFIG_AUX_PHID => true,
       self::CONFIG_SERIALIZATION => array(
         'details' => self::SERIALIZATION_JSON,
       ),
     ) + parent::getConfiguration();
   }
 
   public function generatePHID() {
     return PhabricatorPHID::generateNewPHID(
       PhabricatorPHIDConstants::PHID_TYPE_REPO);
   }
 
   public function getDetail($key, $default = null) {
     return idx($this->details, $key, $default);
   }
 
   public function setDetail($key, $value) {
     $this->details[$key] = $value;
     return $this;
   }
 
   public function getDiffusionBrowseURIForPath($path) {
     $drequest = DiffusionRequest::newFromDictionary(
       array(
         'repository' => $this,
         'path'       => $path,
       ));
 
     return $drequest->generateURI(
       array(
         'action' => 'browse',
       ));
   }
 
   public static function newPhutilURIFromGitURI($raw_uri) {
     $uri = new PhutilURI($raw_uri);
     if (!$uri->getProtocol()) {
       if (strpos($raw_uri, '/') === 0) {
         // If the URI starts with a '/', it's an implicit file:// URI on the
         // local disk.
         $uri = new PhutilURI('file://'.$raw_uri);
       } else if (strpos($raw_uri, ':') !== false) {
         // If there's no protocol (git implicit SSH) but the URI has a colon,
         // it's a git implicit SSH URI. Reformat the URI to be a normal URI.
         // These git URIs look like "user@domain.com:path" instead of
         // "ssh://user@domain/path".
         list($domain, $path) = explode(':', $raw_uri, 2);
         $uri = new PhutilURI('ssh://'.$domain.'/'.$path);
       } else {
         throw new Exception("The Git URI '{$raw_uri}' could not be parsed.");
       }
     }
 
     return $uri;
   }
 
   public function getRemoteURI() {
     $raw_uri = $this->getDetail('remote-uri');
     if (!$raw_uri) {
       return null;
     }
 
     $vcs = $this->getVersionControlSystem();
     $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
 
     if ($is_git) {
       $uri = self::newPhutilURIFromGitURI($raw_uri);
     } else {
       $uri = new PhutilURI($raw_uri);
     }
 
     if ($this->isSSHProtocol($uri->getProtocol())) {
       if ($this->getSSHLogin()) {
         $uri->setUser($this->getSSHLogin());
       }
     }
 
     return (string)$uri;
   }
 
   public function getLocalPath() {
     return $this->getDetail('local-path');
   }
 
   public function execRemoteCommand($pattern /*, $arg, ... */) {
     $args = func_get_args();
     $args = $this->formatRemoteCommand($args);
     return call_user_func_array('exec_manual', $args);
   }
 
   public function execxRemoteCommand($pattern /*, $arg, ... */) {
     $args = func_get_args();
     $args = $this->formatRemoteCommand($args);
     return call_user_func_array('execx', $args);
   }
 
   public function getRemoteCommandFuture($pattern /*, $arg, ... */) {
     $args = func_get_args();
     $args = $this->formatRemoteCommand($args);
     return newv('ExecFuture', $args);
   }
 
   public function passthruRemoteCommand($pattern /*, $arg, ... */) {
     $args = func_get_args();
     $args = $this->formatRemoteCommand($args);
     return call_user_func_array('phutil_passthru', $args);
   }
 
   public function execLocalCommand($pattern /*, $arg, ... */) {
     $args = func_get_args();
     $args = $this->formatLocalCommand($args);
     return call_user_func_array('exec_manual', $args);
   }
 
   public function execxLocalCommand($pattern /*, $arg, ... */) {
     $args = func_get_args();
     $args = $this->formatLocalCommand($args);
     return call_user_func_array('execx', $args);
   }
 
   public function getLocalCommandFuture($pattern /*, $arg, ... */) {
     $args = func_get_args();
     $args = $this->formatLocalCommand($args);
     return newv('ExecFuture', $args);
 
   }
   public function passthruLocalCommand($pattern /*, $arg, ... */) {
     $args = func_get_args();
     $args = $this->formatLocalCommand($args);
     return call_user_func_array('phutil_passthru', $args);
   }
 
 
   private function formatRemoteCommand(array $args) {
     $pattern = $args[0];
     $args = array_slice($args, 1);
 
     if ($this->shouldUseSSH()) {
       switch ($this->getVersionControlSystem()) {
         case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
           $pattern = "SVN_SSH=%s svn --non-interactive {$pattern}";
           array_unshift(
             $args,
             csprintf(
               'ssh -l %s -i %s',
               $this->getSSHLogin(),
               $this->getSSHKeyfile()));
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
           $command = call_user_func_array(
             'csprintf',
             array_merge(
               array(
                 "(ssh-add %s && git {$pattern})",
                 $this->getSSHKeyfile(),
               ),
               $args));
           $pattern = "ssh-agent sh -c %s";
           $args = array($command);
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
           $pattern = "hg --config ui.ssh=%s {$pattern}";
           array_unshift(
             $args,
             csprintf(
               'ssh -l %s -i %s',
               $this->getSSHLogin(),
               $this->getSSHKeyfile()));
           break;
         default:
           throw new Exception("Unrecognized version control system.");
       }
     } else if ($this->shouldUseHTTP()) {
       switch ($this->getVersionControlSystem()) {
         case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
           $pattern =
             "svn ".
             "--non-interactive ".
             "--no-auth-cache ".
             "--trust-server-cert ".
             "--username %s ".
             "--password %s ".
             $pattern;
           array_unshift(
             $args,
             $this->getDetail('http-login'),
             $this->getDetail('http-pass'));
           break;
         default:
           throw new Exception(
             "No support for HTTP Basic Auth in this version control system.");
       }
     } else if ($this->shouldUseSVNProtocol()) {
       switch ($this->getVersionControlSystem()) {
         case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
             $pattern =
               "svn ".
               "--non-interactive ".
               "--no-auth-cache ".
               "--username %s ".
               "--password %s ".
               $pattern;
             array_unshift(
               $args,
               $this->getDetail('http-login'),
               $this->getDetail('http-pass'));
             break;
         default:
           throw new Exception(
             "SVN protocol is SVN only.");
       }
     } else {
       switch ($this->getVersionControlSystem()) {
         case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
           $pattern = "svn --non-interactive {$pattern}";
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
           $pattern = "git {$pattern}";
           break;
         case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
           $pattern = "hg {$pattern}";
           break;
         default:
           throw new Exception("Unrecognized version control system.");
       }
     }
 
     array_unshift($args, $pattern);
 
     return $args;
   }
 
   private function formatLocalCommand(array $args) {
     $pattern = $args[0];
     $args = array_slice($args, 1);
 
     switch ($this->getVersionControlSystem()) {
       case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN:
         $pattern = "(cd %s && svn --non-interactive {$pattern})";
         array_unshift($args, $this->getLocalPath());
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT:
         $pattern = "(cd %s && git {$pattern})";
         array_unshift($args, $this->getLocalPath());
         break;
       case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL:
         $pattern = "(cd %s && HGPLAIN=1 hg {$pattern})";
         array_unshift($args, $this->getLocalPath());
         break;
       default:
         throw new Exception("Unrecognized version control system.");
     }
 
     array_unshift($args, $pattern);
 
     return $args;
   }
 
   private function getSSHLogin() {
     return $this->getDetail('ssh-login');
   }
 
   private function getSSHKeyfile() {
     if ($this->sshKeyfile === null) {
       $key = $this->getDetail('ssh-key');
       $keyfile = $this->getDetail('ssh-keyfile');
       if ($keyfile) {
         // Make sure we can read the file, that it exists, etc.
         Filesystem::readFile($keyfile);
         $this->sshKeyfile = $keyfile;
       } else if ($key) {
         $keyfile = new TempFile('phabricator-repository-ssh-key');
         chmod($keyfile, 0600);
         Filesystem::writeFile($keyfile, $key);
         $this->sshKeyfile = $keyfile;
       } else {
         $this->sshKeyfile = '';
       }
     }
 
     return (string)$this->sshKeyfile;
   }
 
   public function shouldUseSSH() {
     $uri = new PhutilURI($this->getRemoteURI());
     $protocol = $uri->getProtocol();
     if ($this->isSSHProtocol($protocol)) {
       return (bool)$this->getSSHKeyfile();
     } else {
       return false;
     }
   }
 
   public function shouldUseHTTP() {
     $uri = new PhutilURI($this->getRemoteURI());
     $protocol = $uri->getProtocol();
     if ($this->isHTTPProtocol($protocol)) {
       return (bool)$this->getDetail('http-login');
     } else {
       return false;
     }
   }
 
   public function shouldUseSVNProtocol() {
     $uri = new PhutilURI($this->getRemoteURI());
     $protocol = $uri->getProtocol();
     if ($this->isSVNProtocol($protocol)) {
       return (bool)$this->getDetail('http-login');
     } else {
       return false;
     }
   }
 
 
   public function getPublicRemoteURI() {
     $uri = new PhutilURI($this->getRemoteURI());
 
     // Make sure we don't leak anything if this repo is using HTTP Basic Auth
     // with the credentials in the URI or something zany like that.
     $uri->setUser(null);
     $uri->setPass(null);
 
     return $uri;
   }
 
   public function getURI() {
     return '/diffusion/'.$this->getCallsign().'/';
   }
 
   private function isSSHProtocol($protocol) {
     return ($protocol == 'ssh' || $protocol == 'svn+ssh');
   }
 
   private function isHTTPProtocol($protocol) {
     return ($protocol == 'http' || $protocol == 'https');
   }
 
   private function isSVNProtocol($protocol) {
     return ($protocol == 'svn');
   }
 
   public function isTracked() {
     return $this->getDetail('tracking-enabled', false);
   }
 
   public function getDefaultBranch() {
     $default = $this->getDetail('default-branch');
     if (strlen($default)) {
       return $default;
     }
 
     $default_branches = array(
       PhabricatorRepositoryType::REPOSITORY_TYPE_GIT        => 'master',
       PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL  => 'default',
     );
 
     return idx($default_branches, $this->getVersionControlSystem());
   }
 
-  public function shouldTrackBranch($branch) {
+  private function isBranchInFilter($branch, $filter_key) {
     $vcs = $this->getVersionControlSystem();
 
     $is_git = ($vcs == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT);
 
     $use_filter = ($is_git);
 
     if ($use_filter) {
-      $filter = $this->getDetail('branch-filter', array());
-      if ($filter && !isset($filter[$branch])) {
+      $filter = $this->getDetail($filter_key, array());
+      if ($filter && empty($filter[$branch])) {
         return false;
       }
     }
 
-    // By default, track all branches.
+    // By default, all branches pass.
     return true;
   }
 
+  public function shouldTrackBranch($branch) {
+    return $this->isBranchInFilter($branch, 'branch-filter');
+  }
+
+  public function shouldAutocloseBranch($branch) {
+    return $this->isBranchInFilter($branch, 'close-commits-filter');
+  }
+
+  public function shouldAutocloseCommit(
+    PhabricatorRepositoryCommit $commit,
+    PhabricatorRepositoryCommitData $data) {
+
+    if ($this->getDetail('disable-autoclose', false)) {
+      return false;
+    }
+
+    $branches = $data->getCommitDetail('seenOnBranches', array());
+    foreach ($branches as $branch) {
+      if ($this->shouldAutocloseBranch($branch)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
   public function formatCommitName($commit_identifier) {
     $vcs = $this->getVersionControlSystem();
 
     $type_git = PhabricatorRepositoryType::REPOSITORY_TYPE_GIT;
     $type_hg = PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL;
 
     $is_git = ($vcs == $type_git);
     $is_hg = ($vcs == $type_hg);
     if ($is_git || $is_hg) {
       $short_identifier = substr($commit_identifier, 0, 12);
     } else {
       $short_identifier = $commit_identifier;
     }
 
     return 'r'.$this->getCallsign().$short_identifier;
   }
 
   public static function loadAllByPHIDOrCallsign(array $names) {
     $repositories = array();
     foreach ($names as $name) {
       $repo = id(new PhabricatorRepository())->loadOneWhere(
         'phid = %s OR callsign = %s',
         $name,
         $name);
       if (!$repo) {
         throw new Exception(
           "No repository with PHID or callsign '{$name}' exists!");
       }
       $repositories[$repo->getID()] = $repo;
     }
     return $repositories;
   }
 
 }
diff --git a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php
index 663091f6d9..5fbd96cbc1 100644
--- a/src/applications/repository/storage/PhabricatorRepositoryCommitData.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryCommitData.php
@@ -1,56 +1,56 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 final class PhabricatorRepositoryCommitData extends PhabricatorRepositoryDAO {
 
   const SUMMARY_MAX_LENGTH = 100;
 
   protected $commitID;
-  protected $authorName;
-  protected $commitMessage;
+  protected $authorName    = '';
+  protected $commitMessage = '';
   protected $commitDetails = array();
 
   public function getConfiguration() {
     return array(
       self::CONFIG_TIMESTAMPS => false,
       self::CONFIG_SERIALIZATION => array(
         'commitDetails' => self::SERIALIZATION_JSON,
       ),
     ) + parent::getConfiguration();
   }
 
   public function getSummary() {
     $message = $this->getCommitMessage();
     $lines = explode("\n", $message);
     $summary = head($lines);
 
     $summary = phutil_utf8_shorten($summary, self::SUMMARY_MAX_LENGTH);
 
     return $summary;
   }
 
   public function getCommitDetail($key, $default = null) {
     return idx($this->commitDetails, $key, $default);
   }
 
   public function setCommitDetail($key, $value) {
     $this->commitDetails[$key] = $value;
     return $this;
   }
 
 }
diff --git a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
index 03a7f519b2..7492a082dd 100644
--- a/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
+++ b/src/applications/repository/worker/commitmessageparser/PhabricatorRepositoryCommitMessageParserWorker.php
@@ -1,332 +1,332 @@
 <?php
 
 /*
  * Copyright 2012 Facebook, Inc.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
  * You may obtain a copy of the License at
  *
  *   http://www.apache.org/licenses/LICENSE-2.0
  *
  * Unless required by applicable law or agreed to in writing, software
  * distributed under the License is distributed on an "AS IS" BASIS,
  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
 
 abstract class PhabricatorRepositoryCommitMessageParserWorker
   extends PhabricatorRepositoryCommitParserWorker {
 
   abstract protected function getCommitHashes(
     PhabricatorRepository $repository,
     PhabricatorRepositoryCommit $commit);
 
   final protected function updateCommitData($author, $message,
     $committer = null) {
 
     $commit = $this->commit;
 
     $data = id(new PhabricatorRepositoryCommitData())->loadOneWhere(
       'commitID = %d',
       $commit->getID());
     if (!$data) {
       $data = new PhabricatorRepositoryCommitData();
     }
     $data->setCommitID($commit->getID());
     $data->setAuthorName($author);
     $data->setCommitMessage($message);
 
     if ($committer) {
       $data->setCommitDetail('committer', $committer);
     }
 
     $repository = $this->repository;
     $detail_parser = $repository->getDetail(
       'detail-parser',
       'PhabricatorRepositoryDefaultCommitMessageDetailParser');
 
     if ($detail_parser) {
       $parser_obj = newv($detail_parser, array($commit, $data));
       $parser_obj->parseCommitDetails();
     }
 
     $author_phid = $data->getCommitDetail('authorPHID');
     if ($author_phid) {
       $commit->setAuthorPHID($author_phid);
       $commit->save();
     }
 
     $conn_w = id(new DifferentialRevision())->establishConnection('w');
 
     // NOTE: The `differential_commit` table has a unique ID on `commitPHID`,
     // preventing more than one revision from being associated with a commit.
     // Generally this is good and desirable, but with the advent of hash
     // tracking we may end up in a situation where we match several different
     // revisions. We just kind of ignore this and pick one, we might want to
     // revisit this and do something differently. (If we match several revisions
     // someone probably did something very silly, though.)
 
     $revision_id = $data->getCommitDetail('differential.revisionID');
     if (!$revision_id) {
       $hashes = $this->getCommitHashes(
         $this->repository,
         $this->commit);
       if ($hashes) {
 
         $query = new DifferentialRevisionQuery();
         $query->withCommitHashes($hashes);
         $revisions = $query->execute();
 
         if (!empty($revisions)) {
           $revision = $this->identifyBestRevision($revisions);
           $revision_id = $revision->getID();
         }
       }
     }
 
     if ($revision_id) {
       $revision = id(new DifferentialRevision())->load($revision_id);
       if ($revision) {
         queryfx(
           $conn_w,
           'INSERT IGNORE INTO %T (revisionID, commitPHID) VALUES (%d, %s)',
           DifferentialRevision::TABLE_COMMIT,
           $revision->getID(),
           $commit->getPHID());
         $commit_is_new = $conn_w->getAffectedRows();
 
         $message = null;
         $name = $data->getCommitDetail('committer');
         if ($name !== null) {
           $committer = $data->getCommitDetail('committerPHID');
         } else {
           $committer = $data->getCommitDetail('authorPHID');
           $name = $data->getAuthorName();
         }
         if (!$committer) {
           $committer = $revision->getAuthorPHID();
           $message = 'Closed by '.$name.'.';
         }
 
         if ($commit_is_new) {
           $diff = $this->attachToRevision($revision, $committer);
         }
 
         $status_closed = ArcanistDifferentialRevisionStatus::CLOSED;
         $should_close = ($revision->getStatus() != $status_closed) &&
-                        (!$repository->getDetail('disable-autoclose', false));
+                        $repository->shouldAutocloseCommit($commit, $data);
 
         if ($should_close) {
           $revision->setDateCommitted($commit->getEpoch());
           $editor = new DifferentialCommentEditor(
             $revision,
             $committer,
             DifferentialAction::ACTION_CLOSE);
           $editor->setIsDaemonWorkflow(true);
 
           if ($commit_is_new) {
             $vs_diff = $this->loadChangedByCommit($diff);
             if ($vs_diff) {
               $data->setCommitDetail('vsDiff', $vs_diff->getID());
 
               $changed_by_commit = PhabricatorEnv::getProductionURI(
                 '/D'.$revision->getID().
                 '?vs='.$vs_diff->getID().
                 '&id='.$diff->getID().
                 '#differential-review-toc');
               $editor->setChangedByCommit($changed_by_commit);
             }
           }
 
           $editor->setMessage($message)->save();
         }
 
       }
     }
 
     $data->save();
   }
 
   private function attachToRevision(
     DifferentialRevision $revision,
     $committer) {
 
     $drequest = DiffusionRequest::newFromDictionary(array(
       'repository' => $this->repository,
       'commit' => $this->commit->getCommitIdentifier(),
     ));
 
     $raw_diff = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest)
       ->loadRawDiff();
 
     $changes = id(new ArcanistDiffParser())->parseDiff($raw_diff);
     $diff = DifferentialDiff::newFromRawChanges($changes)
       ->setRevisionID($revision->getID())
       ->setAuthorPHID($committer)
       ->setCreationMethod('commit')
       ->setSourceControlSystem($this->repository->getVersionControlSystem())
       ->setLintStatus(DifferentialLintStatus::LINT_SKIP)
       ->setUnitStatus(DifferentialUnitStatus::UNIT_SKIP)
       ->setDateCreated($this->commit->getEpoch())
       ->setDescription(
         'Commit r'.
         $this->repository->getCallsign().
         $this->commit->getCommitIdentifier());
 
     // TODO: This is not correct in SVN where one repository can have multiple
     // Arcanist projects.
     $arcanist_project = id(new PhabricatorRepositoryArcanistProject())
       ->loadOneWhere('repositoryID = %d LIMIT 1', $this->repository->getID());
     if ($arcanist_project) {
       $diff->setArcanistProjectPHID($arcanist_project->getPHID());
     }
 
     $parents = DiffusionCommitParentsQuery::newFromDiffusionRequest($drequest)
       ->loadParents();
     if ($parents) {
       $diff->setSourceControlBaseRevision(head_key($parents));
     }
 
     // TODO: Attach binary files.
 
     return $diff->save();
   }
 
   private function loadChangedByCommit(DifferentialDiff $diff) {
     $repository = $this->repository;
 
     $vs_changesets = array();
     $vs_diff = id(new DifferentialDiff())->loadOneWhere(
       'revisionID = %d AND creationMethod != %s ORDER BY id DESC LIMIT 1',
       $diff->getRevisionID(),
       'commit');
     foreach ($vs_diff->loadChangesets() as $changeset) {
       $path = $changeset->getAbsoluteRepositoryPath($repository, $vs_diff);
       $vs_changesets[$path] = $changeset;
     }
 
     $changesets = array();
     foreach ($diff->getChangesets() as $changeset) {
       $path = $changeset->getAbsoluteRepositoryPath($repository, $diff);
       $changesets[$path] = $changeset;
     }
 
     if (array_fill_keys(array_keys($changesets), true) !=
         array_fill_keys(array_keys($vs_changesets), true)) {
       return $vs_diff;
     }
 
     $hunks = id(new DifferentialHunk())->loadAllWhere(
       'changesetID IN (%Ld)',
       mpull($vs_changesets, 'getID'));
     $hunks = mgroup($hunks, 'getChangesetID');
     foreach ($vs_changesets as $changeset) {
       $changeset->attachHunks(idx($hunks, $changeset->getID(), array()));
     }
 
     $file_phids = array();
     foreach ($vs_changesets as $changeset) {
       $metadata = $changeset->getMetadata();
       $file_phid = idx($metadata, 'new:binary-phid');
       if ($file_phid) {
         $file_phids[$file_phid] = $file_phid;
       }
     }
 
     $files = array();
     if ($file_phids) {
       $files = id(new PhabricatorFile())->loadAllWhere(
         'phid IN (%Ls)',
         $file_phids);
       $files = mpull($files, null, 'getPHID');
     }
 
     foreach ($changesets as $path => $changeset) {
       $vs_changeset = $vs_changesets[$path];
 
       $file_phid = idx($vs_changeset->getMetadata(), 'new:binary-phid');
       if ($file_phid) {
         if (!isset($files[$file_phid])) {
           return $vs_diff;
         }
         $drequest = DiffusionRequest::newFromDictionary(array(
           'repository' => $this->repository,
           'commit' => $this->commit->getCommitIdentifier(),
           'path' => $path,
         ));
         $corpus = DiffusionFileContentQuery::newFromDiffusionRequest($drequest)
           ->loadFileContent()
           ->getCorpus();
         if ($files[$file_phid]->loadFileData() != $corpus) {
           return $vs_diff;
         }
       } else if ($changeset->makeChangesWithContext() !=
           $vs_changeset->makeChangesWithContext()) {
         return $vs_diff;
       }
     }
 
     return null;
   }
 
   /**
    * When querying for revisions by hash, more than one revision may be found.
    * This function identifies the "best" revision from such a set.  Typically,
    * there is only one revision found.   Otherwise, we try to pick an accepted
    * revision first, followed by an open revision, and otherwise we go with a
    * closed or abandoned revision as a last resort.
    */
   private function identifyBestRevision(array $revisions) {
     assert_instances_of($revisions, 'DifferentialRevision');
     // get the simplest, common case out of the way
     if (count($revisions) == 1) {
       return reset($revisions);
     }
 
     $first_choice = array();
     $second_choice = array();
     $third_choice = array();
     foreach ($revisions as $revision) {
       switch ($revision->getStatus()) {
         // "Accepted" revisions -- ostensibly what we're looking for!
         case ArcanistDifferentialRevisionStatus::ACCEPTED:
           $first_choice[] = $revision;
           break;
         // "Open" revisions
         case ArcanistDifferentialRevisionStatus::NEEDS_REVIEW:
         case ArcanistDifferentialRevisionStatus::NEEDS_REVISION:
           $second_choice[] = $revision;
           break;
         // default is a wtf? here
         default:
         case ArcanistDifferentialRevisionStatus::ABANDONED:
         case ArcanistDifferentialRevisionStatus::CLOSED:
           $third_choice[] = $revision;
           break;
       }
     }
 
     // go down the ladder like a bro at last call
     if (!empty($first_choice)) {
       return $this->identifyMostRecentRevision($first_choice);
     }
     if (!empty($second_choice)) {
       return $this->identifyMostRecentRevision($second_choice);
     }
     if (!empty($third_choice)) {
       return $this->identifyMostRecentRevision($third_choice);
     }
   }
 
   /**
    * Given a set of revisions, returns the revision with the latest
    * updated time.   This is ostensibly the most recent revision.
    */
   private function identifyMostRecentRevision(array $revisions) {
     assert_instances_of($revisions, 'DifferentialRevision');
     $revisions = msort($revisions, 'getDateModified');
     return end($revisions);
   }
 }