diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php
--- a/src/__phutil_library_map__.php
+++ b/src/__phutil_library_map__.php
@@ -4081,8 +4081,13 @@
     'PhabricatorRemarkupUIExample' => 'applications/uiexample/examples/PhabricatorRemarkupUIExample.php',
     'PhabricatorRepositoriesSetupCheck' => 'applications/config/check/PhabricatorRepositoriesSetupCheck.php',
     'PhabricatorRepository' => 'applications/repository/storage/PhabricatorRepository.php',
+    'PhabricatorRepositoryActivateTransaction' => 'applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php',
     'PhabricatorRepositoryAuditRequest' => 'applications/repository/storage/PhabricatorRepositoryAuditRequest.php',
+    'PhabricatorRepositoryAutocloseOnlyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php',
+    'PhabricatorRepositoryAutocloseTransaction' => 'applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php',
+    'PhabricatorRepositoryBlueprintsTransaction' => 'applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php',
     'PhabricatorRepositoryBranch' => 'applications/repository/storage/PhabricatorRepositoryBranch.php',
+    'PhabricatorRepositoryCallsignTransaction' => 'applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php',
     'PhabricatorRepositoryCommit' => 'applications/repository/storage/PhabricatorRepositoryCommit.php',
     'PhabricatorRepositoryCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryCommitChangeParserWorker.php',
     'PhabricatorRepositoryCommitData' => 'applications/repository/storage/PhabricatorRepositoryCommitData.php',
@@ -4096,10 +4101,15 @@
     'PhabricatorRepositoryCommitTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryCommitTestCase.php',
     'PhabricatorRepositoryConfigOptions' => 'applications/repository/config/PhabricatorRepositoryConfigOptions.php',
     'PhabricatorRepositoryDAO' => 'applications/repository/storage/PhabricatorRepositoryDAO.php',
+    'PhabricatorRepositoryDangerousTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php',
+    'PhabricatorRepositoryDefaultBranchTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php',
+    'PhabricatorRepositoryDescriptionTransaction' => 'applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php',
     'PhabricatorRepositoryDestructibleCodex' => 'applications/repository/codex/PhabricatorRepositoryDestructibleCodex.php',
     'PhabricatorRepositoryDiscoveryEngine' => 'applications/repository/engine/PhabricatorRepositoryDiscoveryEngine.php',
     'PhabricatorRepositoryEditor' => 'applications/repository/editor/PhabricatorRepositoryEditor.php',
+    'PhabricatorRepositoryEncodingTransaction' => 'applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php',
     'PhabricatorRepositoryEngine' => 'applications/repository/engine/PhabricatorRepositoryEngine.php',
+    'PhabricatorRepositoryEnormousTransaction' => 'applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php',
     'PhabricatorRepositoryFerretEngine' => 'applications/repository/search/PhabricatorRepositoryFerretEngine.php',
     'PhabricatorRepositoryFulltextEngine' => 'applications/repository/search/PhabricatorRepositoryFulltextEngine.php',
     'PhabricatorRepositoryGitCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositoryGitCommitChangeParserWorker.php',
@@ -4143,6 +4153,8 @@
     'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositoryMercurialCommitMessageParserWorker.php',
     'PhabricatorRepositoryMirror' => 'applications/repository/storage/PhabricatorRepositoryMirror.php',
     'PhabricatorRepositoryMirrorEngine' => 'applications/repository/engine/PhabricatorRepositoryMirrorEngine.php',
+    'PhabricatorRepositoryNameTransaction' => 'applications/repository/xaction/PhabricatorRepositoryNameTransaction.php',
+    'PhabricatorRepositoryNotifyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php',
     'PhabricatorRepositoryOldRef' => 'applications/repository/storage/PhabricatorRepositoryOldRef.php',
     'PhabricatorRepositoryParsedChange' => 'applications/repository/data/PhabricatorRepositoryParsedChange.php',
     'PhabricatorRepositoryPullEngine' => 'applications/repository/engine/PhabricatorRepositoryPullEngine.php',
@@ -4159,6 +4171,7 @@
     'PhabricatorRepositoryPushLogQuery' => 'applications/repository/query/PhabricatorRepositoryPushLogQuery.php',
     'PhabricatorRepositoryPushLogSearchEngine' => 'applications/repository/query/PhabricatorRepositoryPushLogSearchEngine.php',
     'PhabricatorRepositoryPushMailWorker' => 'applications/repository/worker/PhabricatorRepositoryPushMailWorker.php',
+    'PhabricatorRepositoryPushPolicyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php',
     'PhabricatorRepositoryPushReplyHandler' => 'applications/repository/mail/PhabricatorRepositoryPushReplyHandler.php',
     'PhabricatorRepositoryQuery' => 'applications/repository/query/PhabricatorRepositoryQuery.php',
     'PhabricatorRepositoryRefCursor' => 'applications/repository/storage/PhabricatorRepositoryRefCursor.php',
@@ -4167,18 +4180,26 @@
     'PhabricatorRepositoryRefEngine' => 'applications/repository/engine/PhabricatorRepositoryRefEngine.php',
     'PhabricatorRepositoryRefPosition' => 'applications/repository/storage/PhabricatorRepositoryRefPosition.php',
     'PhabricatorRepositoryRepositoryPHIDType' => 'applications/repository/phid/PhabricatorRepositoryRepositoryPHIDType.php',
+    'PhabricatorRepositorySVNSubpathTransaction' => 'applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php',
     'PhabricatorRepositorySchemaSpec' => 'applications/repository/storage/PhabricatorRepositorySchemaSpec.php',
     'PhabricatorRepositorySearchEngine' => 'applications/repository/query/PhabricatorRepositorySearchEngine.php',
+    'PhabricatorRepositoryServiceTransaction' => 'applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php',
+    'PhabricatorRepositorySlugTransaction' => 'applications/repository/xaction/PhabricatorRepositorySlugTransaction.php',
+    'PhabricatorRepositoryStagingURITransaction' => 'applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php',
     'PhabricatorRepositoryStatusMessage' => 'applications/repository/storage/PhabricatorRepositoryStatusMessage.php',
     'PhabricatorRepositorySvnCommitChangeParserWorker' => 'applications/repository/worker/commitchangeparser/PhabricatorRepositorySvnCommitChangeParserWorker.php',
     'PhabricatorRepositorySvnCommitMessageParserWorker' => 'applications/repository/worker/commitmessageparser/PhabricatorRepositorySvnCommitMessageParserWorker.php',
     'PhabricatorRepositorySymbol' => 'applications/repository/storage/PhabricatorRepositorySymbol.php',
+    'PhabricatorRepositorySymbolLanguagesTransaction' => 'applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php',
+    'PhabricatorRepositorySymbolSourcesTransaction' => 'applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php',
     'PhabricatorRepositorySyncEvent' => 'applications/repository/storage/PhabricatorRepositorySyncEvent.php',
     'PhabricatorRepositorySyncEventPHIDType' => 'applications/repository/phid/PhabricatorRepositorySyncEventPHIDType.php',
     'PhabricatorRepositorySyncEventQuery' => 'applications/repository/query/PhabricatorRepositorySyncEventQuery.php',
     'PhabricatorRepositoryTestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryTestCase.php',
+    'PhabricatorRepositoryTrackOnlyTransaction' => 'applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php',
     'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php',
     'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php',
+    'PhabricatorRepositoryTransactionType' => 'applications/repository/xaction/PhabricatorRepositoryTransactionType.php',
     'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php',
     'PhabricatorRepositoryURI' => 'applications/repository/storage/PhabricatorRepositoryURI.php',
     'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php',
@@ -4189,6 +4210,7 @@
     'PhabricatorRepositoryURITestCase' => 'applications/repository/storage/__tests__/PhabricatorRepositoryURITestCase.php',
     'PhabricatorRepositoryURITransaction' => 'applications/repository/storage/PhabricatorRepositoryURITransaction.php',
     'PhabricatorRepositoryURITransactionQuery' => 'applications/repository/query/PhabricatorRepositoryURITransactionQuery.php',
+    'PhabricatorRepositoryVCSTransaction' => 'applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php',
     'PhabricatorRepositoryWorkingCopyVersion' => 'applications/repository/storage/PhabricatorRepositoryWorkingCopyVersion.php',
     'PhabricatorRequestExceptionHandler' => 'aphront/handler/PhabricatorRequestExceptionHandler.php',
     'PhabricatorResourceSite' => 'aphront/site/PhabricatorResourceSite.php',
@@ -9996,11 +10018,16 @@
       'PhabricatorFulltextInterface',
       'PhabricatorFerretInterface',
     ),
+    'PhabricatorRepositoryActivateTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryAuditRequest' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
     ),
+    'PhabricatorRepositoryAutocloseOnlyTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositoryAutocloseTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositoryBlueprintsTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryBranch' => 'PhabricatorRepositoryDAO',
+    'PhabricatorRepositoryCallsignTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryCommit' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
@@ -10034,10 +10061,15 @@
     'PhabricatorRepositoryCommitTestCase' => 'PhabricatorTestCase',
     'PhabricatorRepositoryConfigOptions' => 'PhabricatorApplicationConfigOptions',
     'PhabricatorRepositoryDAO' => 'PhabricatorLiskDAO',
+    'PhabricatorRepositoryDangerousTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositoryDefaultBranchTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositoryDescriptionTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryDestructibleCodex' => 'PhabricatorDestructibleCodex',
     'PhabricatorRepositoryDiscoveryEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryEditor' => 'PhabricatorApplicationTransactionEditor',
+    'PhabricatorRepositoryEncodingTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryEngine' => 'Phobject',
+    'PhabricatorRepositoryEnormousTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryFerretEngine' => 'PhabricatorFerretEngine',
     'PhabricatorRepositoryFulltextEngine' => 'PhabricatorFulltextEngine',
     'PhabricatorRepositoryGitCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
@@ -10089,6 +10121,8 @@
     'PhabricatorRepositoryMercurialCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
     'PhabricatorRepositoryMirror' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryMirrorEngine' => 'PhabricatorRepositoryEngine',
+    'PhabricatorRepositoryNameTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositoryNotifyTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryOldRef' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
@@ -10117,6 +10151,7 @@
     'PhabricatorRepositoryPushLogQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryPushLogSearchEngine' => 'PhabricatorApplicationSearchEngine',
     'PhabricatorRepositoryPushMailWorker' => 'PhabricatorWorker',
+    'PhabricatorRepositoryPushPolicyTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryPushReplyHandler' => 'PhabricatorMailReplyHandler',
     'PhabricatorRepositoryQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryRefCursor' => array(
@@ -10128,12 +10163,18 @@
     'PhabricatorRepositoryRefEngine' => 'PhabricatorRepositoryEngine',
     'PhabricatorRepositoryRefPosition' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositoryRepositoryPHIDType' => 'PhabricatorPHIDType',
+    'PhabricatorRepositorySVNSubpathTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositorySchemaSpec' => 'PhabricatorConfigSchemaSpec',
     'PhabricatorRepositorySearchEngine' => 'PhabricatorApplicationSearchEngine',
+    'PhabricatorRepositoryServiceTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositorySlugTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositoryStagingURITransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryStatusMessage' => 'PhabricatorRepositoryDAO',
     'PhabricatorRepositorySvnCommitChangeParserWorker' => 'PhabricatorRepositoryCommitChangeParserWorker',
     'PhabricatorRepositorySvnCommitMessageParserWorker' => 'PhabricatorRepositoryCommitMessageParserWorker',
     'PhabricatorRepositorySymbol' => 'PhabricatorRepositoryDAO',
+    'PhabricatorRepositorySymbolLanguagesTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositorySymbolSourcesTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositorySyncEvent' => array(
       'PhabricatorRepositoryDAO',
       'PhabricatorPolicyInterface',
@@ -10141,8 +10182,10 @@
     'PhabricatorRepositorySyncEventPHIDType' => 'PhabricatorPHIDType',
     'PhabricatorRepositorySyncEventQuery' => 'PhabricatorCursorPagedPolicyAwareQuery',
     'PhabricatorRepositoryTestCase' => 'PhabricatorTestCase',
-    'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction',
+    'PhabricatorRepositoryTrackOnlyTransaction' => 'PhabricatorRepositoryTransactionType',
+    'PhabricatorRepositoryTransaction' => 'PhabricatorModularTransaction',
     'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PhabricatorRepositoryTransactionType' => 'PhabricatorModularTransactionType',
     'PhabricatorRepositoryType' => 'Phobject',
     'PhabricatorRepositoryURI' => array(
       'PhabricatorRepositoryDAO',
@@ -10159,6 +10202,7 @@
     'PhabricatorRepositoryURITestCase' => 'PhabricatorTestCase',
     'PhabricatorRepositoryURITransaction' => 'PhabricatorApplicationTransaction',
     'PhabricatorRepositoryURITransactionQuery' => 'PhabricatorApplicationTransactionQuery',
+    'PhabricatorRepositoryVCSTransaction' => 'PhabricatorRepositoryTransactionType',
     'PhabricatorRepositoryWorkingCopyVersion' => 'PhabricatorRepositoryDAO',
     'PhabricatorRequestExceptionHandler' => 'AphrontRequestExceptionHandler',
     'PhabricatorResourceSite' => 'PhabricatorSite',
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditActivateController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditActivateController.php
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditActivateController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditActivateController.php
@@ -25,7 +25,8 @@
       }
 
       $xaction = id(new PhabricatorRepositoryTransaction())
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ACTIVATE)
+        ->setTransactionType(
+          PhabricatorRepositoryActivateTransaction::TRANSACTIONTYPE)
         ->setNewValue($new_status);
 
       $editor = id(new PhabricatorRepositoryEditor())
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditDangerousController.php
@@ -30,7 +30,8 @@
 
     if ($request->isFormPost()) {
       $xaction = id(new PhabricatorRepositoryTransaction())
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DANGEROUS)
+        ->setTransactionType(
+          PhabricatorRepositoryDangerousTransaction::TRANSACTIONTYPE)
         ->setNewValue(!$repository->shouldAllowDangerousChanges());
 
       $editor = id(new PhabricatorRepositoryEditor())
diff --git a/src/applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php b/src/applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php
--- a/src/applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php
+++ b/src/applications/diffusion/controller/DiffusionRepositoryEditEnormousController.php
@@ -30,7 +30,8 @@
 
     if ($request->isFormPost()) {
       $xaction = id(new PhabricatorRepositoryTransaction())
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ENORMOUS)
+        ->setTransactionType(
+          PhabricatorRepositoryEnormousTransaction::TRANSACTIONTYPE)
         ->setNewValue(!$repository->shouldAllowEnormousChanges());
 
       $editor = id(new PhabricatorRepositoryEditor())
diff --git a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php
--- a/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php
+++ b/src/applications/diffusion/editor/DiffusionRepositoryEditEngine.php
@@ -243,7 +243,8 @@
       id(new PhabricatorSelectEditField())
         ->setKey('vcs')
         ->setLabel(pht('Version Control System'))
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_VCS)
+        ->setTransactionType(
+          PhabricatorRepositoryVCSTransaction::TRANSACTIONTYPE)
         ->setIsConduitOnly(true)
         ->setIsCopyable(true)
         ->setOptions(PhabricatorRepositoryType::getAllRepositoryTypes())
@@ -258,7 +259,8 @@
         ->setKey('name')
         ->setLabel(pht('Name'))
         ->setIsRequired(true)
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_NAME)
+        ->setTransactionType(
+          PhabricatorRepositoryNameTransaction::TRANSACTIONTYPE)
         ->setDescription(pht('The repository name.'))
         ->setConduitDescription(pht('Rename the repository.'))
         ->setConduitTypeDescription(pht('New repository name.'))
@@ -266,7 +268,8 @@
       id(new PhabricatorTextEditField())
         ->setKey('callsign')
         ->setLabel(pht('Callsign'))
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_CALLSIGN)
+        ->setTransactionType(
+          PhabricatorRepositoryCallsignTransaction::TRANSACTIONTYPE)
         ->setDescription(pht('The repository callsign.'))
         ->setConduitDescription(pht('Change the repository callsign.'))
         ->setConduitTypeDescription(pht('New repository callsign.'))
@@ -274,7 +277,8 @@
       id(new PhabricatorTextEditField())
         ->setKey('shortName')
         ->setLabel(pht('Short Name'))
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_SLUG)
+        ->setTransactionType(
+          PhabricatorRepositorySlugTransaction::TRANSACTIONTYPE)
         ->setDescription(pht('Short, unique repository name.'))
         ->setConduitDescription(pht('Change the repository short name.'))
         ->setConduitTypeDescription(pht('New short name for the repository.'))
@@ -282,7 +286,8 @@
       id(new PhabricatorRemarkupEditField())
         ->setKey('description')
         ->setLabel(pht('Description'))
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DESCRIPTION)
+        ->setTransactionType(
+          PhabricatorRepositoryDescriptionTransaction::TRANSACTIONTYPE)
         ->setDescription(pht('Repository description.'))
         ->setConduitDescription(pht('Change the repository description.'))
         ->setConduitTypeDescription(pht('New repository description.'))
@@ -291,7 +296,8 @@
         ->setKey('encoding')
         ->setLabel(pht('Text Encoding'))
         ->setIsCopyable(true)
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ENCODING)
+        ->setTransactionType(
+          PhabricatorRepositoryEncodingTransaction::TRANSACTIONTYPE)
         ->setDescription(pht('Default text encoding.'))
         ->setConduitDescription(pht('Change the default text encoding.'))
         ->setConduitTypeDescription(pht('New text encoding.'))
@@ -304,7 +310,8 @@
         ->setOptions(
           pht('Prevent Dangerous Changes'),
           pht('Allow Dangerous Changes'))
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_DANGEROUS)
+        ->setTransactionType(
+          PhabricatorRepositoryDangerousTransaction::TRANSACTIONTYPE)
         ->setDescription(pht('Permit dangerous changes to be made.'))
         ->setConduitDescription(pht('Allow or prevent dangerous changes.'))
         ->setConduitTypeDescription(pht('New protection setting.'))
@@ -317,7 +324,8 @@
         ->setOptions(
           pht('Prevent Enormous Changes'),
           pht('Allow Enormous Changes'))
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ENORMOUS)
+        ->setTransactionType(
+          PhabricatorRepositoryEnormousTransaction::TRANSACTIONTYPE)
         ->setDescription(pht('Permit enormous changes to be made.'))
         ->setConduitDescription(pht('Allow or prevent enormous changes.'))
         ->setConduitTypeDescription(pht('New protection setting.'))
@@ -325,7 +333,8 @@
       id(new PhabricatorSelectEditField())
         ->setKey('status')
         ->setLabel(pht('Status'))
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_ACTIVATE)
+        ->setTransactionType(
+          PhabricatorRepositoryActivateTransaction::TRANSACTIONTYPE)
         ->setIsConduitOnly(true)
         ->setOptions(PhabricatorRepository::getStatusNameMap())
         ->setDescription(pht('Active or inactive status.'))
@@ -336,7 +345,7 @@
         ->setKey('defaultBranch')
         ->setLabel(pht('Default Branch'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH)
+          PhabricatorRepositoryDefaultBranchTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setDescription(pht('Default branch name.'))
         ->setConduitDescription(pht('Set the default branch name.'))
@@ -347,7 +356,7 @@
         ->setKey('trackOnly')
         ->setLabel(pht('Track Only'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY)
+          PhabricatorRepositoryTrackOnlyTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setDescription(pht('Track only these branches.'))
         ->setConduitDescription(pht('Set the tracked branches.'))
@@ -358,7 +367,7 @@
         ->setKey('autocloseOnly')
         ->setLabel(pht('Autoclose Only'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY)
+          PhabricatorRepositoryAutocloseOnlyTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setDescription(pht('Autoclose commits on only these branches.'))
         ->setConduitDescription(pht('Set the autoclose branches.'))
@@ -368,7 +377,7 @@
         ->setKey('importOnly')
         ->setLabel(pht('Import Only'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH)
+          PhabricatorRepositorySVNSubpathTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setDescription(pht('Subpath to selectively import.'))
         ->setConduitDescription(pht('Set the subpath to import.'))
@@ -379,7 +388,7 @@
         ->setKey('stagingAreaURI')
         ->setLabel(pht('Staging Area URI'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_STAGING_URI)
+          PhabricatorRepositoryStagingURITransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setDescription(pht('Staging area URI.'))
         ->setConduitDescription(pht('Set the staging area URI.'))
@@ -390,7 +399,7 @@
         ->setKey('automationBlueprintPHIDs')
         ->setLabel(pht('Use Blueprints'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS)
+          PhabricatorRepositoryBlueprintsTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setDatasource(new DrydockBlueprintDatasource())
         ->setDescription(pht('Automation blueprints.'))
@@ -402,7 +411,7 @@
         ->setKey('symbolLanguages')
         ->setLabel(pht('Languages'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE)
+          PhabricatorRepositorySymbolLanguagesTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setDescription(
           pht('Languages which define symbols in this repository.'))
@@ -415,7 +424,7 @@
         ->setKey('symbolRepositoryPHIDs')
         ->setLabel(pht('Uses Symbols From'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES)
+          PhabricatorRepositorySymbolSourcesTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setDatasource(new DiffusionRepositoryDatasource())
         ->setDescription(pht('Repositories to link symbols from.'))
@@ -426,7 +435,7 @@
         ->setKey('publish')
         ->setLabel(pht('Publish/Notify'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_NOTIFY)
+          PhabricatorRepositoryNotifyTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setOptions(
           pht('Disable Notifications, Feed, and Herald'),
@@ -439,7 +448,7 @@
         ->setKey('autoclose')
         ->setLabel(pht('Autoclose'))
         ->setTransactionType(
-          PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE)
+          PhabricatorRepositoryAutocloseTransaction::TRANSACTIONTYPE)
         ->setIsCopyable(true)
         ->setOptions(
           pht('Disable Autoclose'),
@@ -455,7 +464,8 @@
         ->setIsCopyable(true)
         ->setCapability(DiffusionPushCapability::CAPABILITY)
         ->setPolicies($policies)
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY)
+        ->setTransactionType(
+          PhabricatorRepositoryPushPolicyTransaction::TRANSACTIONTYPE)
         ->setDescription(
           pht('Controls who can push changes to the repository.'))
         ->setConduitDescription(
diff --git a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php
--- a/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php
+++ b/src/applications/meta/xactions/PhabricatorApplicationPolicyChangeTransaction.php
@@ -52,8 +52,8 @@
   }
 
   public function getTitle() {
-    $old = $this->renderPolicy($this->getOldValue());
-    $new = $this->renderPolicy($this->getNewValue());
+    $old = $this->renderApplicationPolicy($this->getOldValue());
+    $new = $this->renderApplicationPolicy($this->getNewValue());
 
     return pht(
       '%s changed the "%s" policy from "%s" to "%s".',
@@ -64,8 +64,8 @@
   }
 
   public function getTitleForFeed() {
-    $old = $this->renderPolicy($this->getOldValue());
-    $new = $this->renderPolicy($this->getNewValue());
+    $old = $this->renderApplicationPolicy($this->getOldValue());
+    $new = $this->renderApplicationPolicy($this->getNewValue());
 
     return pht(
       '%s changed the "%s" policy for application %s from "%s" to "%s".',
@@ -165,7 +165,7 @@
     return $errors;
   }
 
-  private function renderPolicy($name) {
+  private function renderApplicationPolicy($name) {
     $policies = $this->getAllPolicies();
     if (empty($policies[$name])) {
       // Not a standard policy, check for a custom policy.
diff --git a/src/applications/repository/editor/PhabricatorRepositoryEditor.php b/src/applications/repository/editor/PhabricatorRepositoryEditor.php
--- a/src/applications/repository/editor/PhabricatorRepositoryEditor.php
+++ b/src/applications/repository/editor/PhabricatorRepositoryEditor.php
@@ -14,28 +14,6 @@
   public function getTransactionTypes() {
     $types = parent::getTransactionTypes();
 
-    $types[] = PhabricatorRepositoryTransaction::TYPE_VCS;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_ACTIVATE;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_NAME;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_DESCRIPTION;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_ENCODING;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_NOTIFY;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_DANGEROUS;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_ENORMOUS;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_SLUG;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_SERVICE;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_STAGING_URI;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS;
-    $types[] = PhabricatorRepositoryTransaction::TYPE_CALLSIGN;
-
     $types[] = PhabricatorTransactions::TYPE_EDGE;
     $types[] = PhabricatorTransactions::TYPE_VIEW_POLICY;
     $types[] = PhabricatorTransactions::TYPE_EDIT_POLICY;
@@ -43,504 +21,6 @@
     return $types;
   }
 
-  protected function getCustomTransactionOldValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhabricatorRepositoryTransaction::TYPE_VCS:
-        return $object->getVersionControlSystem();
-      case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
-        return $object->isTracked();
-      case PhabricatorRepositoryTransaction::TYPE_NAME:
-        return $object->getName();
-      case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION:
-        return $object->getDetail('description');
-      case PhabricatorRepositoryTransaction::TYPE_ENCODING:
-        return $object->getDetail('encoding');
-      case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH:
-        return $object->getDetail('default-branch');
-      case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
-        return array_keys($object->getDetail('branch-filter', array()));
-      case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
-        return array_keys($object->getDetail('close-commits-filter', array()));
-      case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH:
-        return $object->getDetail('svn-subpath');
-      case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
-        return (int)!$object->getDetail('herald-disabled');
-      case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
-        return (int)!$object->getDetail('disable-autoclose');
-      case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
-        return $object->getPushPolicy();
-      case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
-        return $object->shouldAllowDangerousChanges();
-      case PhabricatorRepositoryTransaction::TYPE_ENORMOUS:
-        return $object->shouldAllowEnormousChanges();
-      case PhabricatorRepositoryTransaction::TYPE_SLUG:
-        return $object->getRepositorySlug();
-      case PhabricatorRepositoryTransaction::TYPE_SERVICE:
-        return $object->getAlmanacServicePHID();
-      case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
-        return $object->getSymbolLanguages();
-      case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
-        return $object->getSymbolSources();
-      case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
-        return $object->getDetail('staging-uri');
-      case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
-        return $object->getDetail('automation.blueprintPHIDs', array());
-      case PhabricatorRepositoryTransaction::TYPE_CALLSIGN:
-        return $object->getCallsign();
-    }
-  }
-
-  protected function getCustomTransactionNewValue(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
-      case PhabricatorRepositoryTransaction::TYPE_NAME:
-      case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION:
-      case PhabricatorRepositoryTransaction::TYPE_ENCODING:
-      case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH:
-      case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
-      case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
-      case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH:
-      case PhabricatorRepositoryTransaction::TYPE_VCS:
-      case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
-      case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
-      case PhabricatorRepositoryTransaction::TYPE_ENORMOUS:
-      case PhabricatorRepositoryTransaction::TYPE_SERVICE:
-      case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
-      case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
-      case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
-      case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
-        return $xaction->getNewValue();
-      case PhabricatorRepositoryTransaction::TYPE_SLUG:
-      case PhabricatorRepositoryTransaction::TYPE_CALLSIGN:
-        $name = $xaction->getNewValue();
-        if (strlen($name)) {
-          return $name;
-        }
-        return null;
-      case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
-      case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
-        return (int)$xaction->getNewValue();
-    }
-  }
-
-  protected function applyCustomInternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhabricatorRepositoryTransaction::TYPE_VCS:
-        $object->setVersionControlSystem($xaction->getNewValue());
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
-        $active = $xaction->getNewValue();
-
-        // The first time a repository is activated, clear the "new repository"
-        // flag so we stop showing setup hints.
-        if ($active) {
-          $object->setDetail('newly-initialized', false);
-        }
-
-        $object->setDetail('tracking-enabled', $active);
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_NAME:
-        $object->setName($xaction->getNewValue());
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_DESCRIPTION:
-        $object->setDetail('description', $xaction->getNewValue());
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_DEFAULT_BRANCH:
-        $object->setDetail('default-branch', $xaction->getNewValue());
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
-        $object->setDetail(
-          'branch-filter',
-          array_fill_keys($xaction->getNewValue(), true));
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
-        $object->setDetail(
-          'close-commits-filter',
-          array_fill_keys($xaction->getNewValue(), true));
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_SVN_SUBPATH:
-        $object->setDetail('svn-subpath', $xaction->getNewValue());
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_NOTIFY:
-        $object->setDetail('herald-disabled', (int)!$xaction->getNewValue());
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE:
-        $object->setDetail('disable-autoclose', (int)!$xaction->getNewValue());
-        break;
-      case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
-        return $object->setPushPolicy($xaction->getNewValue());
-      case PhabricatorRepositoryTransaction::TYPE_DANGEROUS:
-        $object->setDetail('allow-dangerous-changes', $xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_ENORMOUS:
-        $object->setDetail('allow-enormous-changes', $xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_SLUG:
-        $object->setRepositorySlug($xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_SERVICE:
-        $object->setAlmanacServicePHID($xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_LANGUAGE:
-        $object->setDetail('symbol-languages', $xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
-        $object->setDetail('symbol-sources', $xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_STAGING_URI:
-        $object->setDetail('staging-uri', $xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
-        $object->setDetail(
-          'automation.blueprintPHIDs',
-          $xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_CALLSIGN:
-        $object->setCallsign($xaction->getNewValue());
-        return;
-      case PhabricatorRepositoryTransaction::TYPE_ENCODING:
-        $object->setDetail('encoding', $xaction->getNewValue());
-        break;
-    }
-  }
-
-  protected function applyCustomExternalTransaction(
-    PhabricatorLiskDAO $object,
-    PhabricatorApplicationTransaction $xaction) {
-
-    switch ($xaction->getTransactionType()) {
-      case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
-        DrydockAuthorization::applyAuthorizationChanges(
-          $this->getActor(),
-          $object->getPHID(),
-          $xaction->getOldValue(),
-          $xaction->getNewValue());
-        break;
-    }
-
-  }
-
-  protected function validateTransaction(
-    PhabricatorLiskDAO $object,
-    $type,
-    array $xactions) {
-
-    $errors = parent::validateTransaction($object, $type, $xactions);
-
-    switch ($type) {
-      case PhabricatorRepositoryTransaction::TYPE_AUTOCLOSE_ONLY:
-      case PhabricatorRepositoryTransaction::TYPE_TRACK_ONLY:
-        foreach ($xactions as $xaction) {
-          foreach ($xaction->getNewValue() as $pattern) {
-            // Check for invalid regular expressions.
-            $regexp = PhabricatorRepository::extractBranchRegexp($pattern);
-            if ($regexp !== null) {
-              $ok = @preg_match($regexp, '');
-              if ($ok === false) {
-                $error = new PhabricatorApplicationTransactionValidationError(
-                  $type,
-                  pht('Invalid'),
-                  pht(
-                    'Expression "%s" is not a valid regular expression. Note '.
-                    'that you must include delimiters.',
-                    $regexp),
-                  $xaction);
-                $errors[] = $error;
-                continue;
-              }
-            }
-
-            // Check for formatting mistakes like `regex(...)` instead of
-            // `regexp(...)`.
-            $matches = null;
-            if (preg_match('/^([^(]+)\\(.*\\)\z/', $pattern, $matches)) {
-              switch ($matches[1]) {
-                case 'regexp':
-                  break;
-                default:
-                  $error = new PhabricatorApplicationTransactionValidationError(
-                    $type,
-                    pht('Invalid'),
-                    pht(
-                      'Matching function "%s(...)" is not recognized. Valid '.
-                      'functions are: regexp(...).',
-                      $matches[1]),
-                    $xaction);
-                  $errors[] = $error;
-                  break;
-              }
-            }
-          }
-        }
-        break;
-
-      case PhabricatorRepositoryTransaction::TYPE_AUTOMATION_BLUEPRINTS:
-        foreach ($xactions as $xaction) {
-          $old = nonempty($xaction->getOldValue(), array());
-          $new = nonempty($xaction->getNewValue(), array());
-
-          $add = array_diff($new, $old);
-
-          $invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer(
-            $this->getActor(),
-            $add);
-          if ($invalid) {
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Invalid'),
-              pht(
-                'Some of the selected automation blueprints are invalid '.
-                'or restricted: %s.',
-                implode(', ', $invalid)),
-              $xaction);
-          }
-        }
-        break;
-
-      case PhabricatorRepositoryTransaction::TYPE_VCS:
-        $vcs_map = PhabricatorRepositoryType::getAllRepositoryTypes();
-        $current_vcs = $object->getVersionControlSystem();
-
-        if (!$this->getIsNewObject()) {
-          foreach ($xactions as $xaction) {
-            if ($xaction->getNewValue() == $current_vcs) {
-              continue;
-            }
-
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Immutable'),
-              pht(
-                'You can not change the version control system an existing '.
-                'repository uses. It can only be set when a repository is '.
-                'first created.'),
-              $xaction);
-          }
-        } else {
-          $value = $object->getVersionControlSystem();
-          foreach ($xactions as $xaction) {
-            $value = $xaction->getNewValue();
-
-            if (empty($vcs_map[$value])) {
-              $errors[] = new PhabricatorApplicationTransactionValidationError(
-                $type,
-                pht('Invalid'),
-                pht(
-                  'Specified version control system must be a VCS '.
-                  'recognized by Phabricator: %s.',
-                  implode(', ', array_keys($vcs_map))),
-                $xaction);
-            }
-          }
-
-          if (!strlen($value)) {
-            $error = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Required'),
-              pht(
-                'When creating a repository, you must specify a valid '.
-                'underlying version control system: %s.',
-                implode(', ', array_keys($vcs_map))),
-              nonempty(last($xactions), null));
-            $error->setIsMissingFieldError(true);
-            $errors[] = $error;
-          }
-        }
-        break;
-
-      case PhabricatorRepositoryTransaction::TYPE_NAME:
-        $missing = $this->validateIsEmptyTextField(
-          $object->getName(),
-          $xactions);
-
-        if ($missing) {
-          $error = new PhabricatorApplicationTransactionValidationError(
-            $type,
-            pht('Required'),
-            pht('Repository name is required.'),
-            nonempty(last($xactions), null));
-
-          $error->setIsMissingFieldError(true);
-          $errors[] = $error;
-        }
-        break;
-
-      case PhabricatorRepositoryTransaction::TYPE_ACTIVATE:
-        $status_map = PhabricatorRepository::getStatusMap();
-        foreach ($xactions as $xaction) {
-          $status = $xaction->getNewValue();
-          if (empty($status_map[$status])) {
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Invalid'),
-              pht(
-                'Repository status "%s" is not valid.',
-                $status),
-              $xaction);
-          }
-        }
-        break;
-
-      case PhabricatorRepositoryTransaction::TYPE_ENCODING:
-        foreach ($xactions as $xaction) {
-          // Make sure the encoding is valid by converting to UTF-8. This tests
-          // that the user has mbstring installed, and also that they didn't
-          // type a garbage encoding name. Note that we're converting from
-          // UTF-8 to the target encoding, because mbstring is fine with
-          // converting from a nonsense encoding.
-          $encoding = $xaction->getNewValue();
-          if (!strlen($encoding)) {
-            continue;
-          }
-
-          try {
-            phutil_utf8_convert('.', $encoding, 'UTF-8');
-          } catch (Exception $ex) {
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Invalid'),
-              pht(
-                'Repository encoding "%s" is not valid: %s',
-                $encoding,
-                $ex->getMessage()),
-              $xaction);
-          }
-        }
-        break;
-
-      case PhabricatorRepositoryTransaction::TYPE_SLUG:
-        foreach ($xactions as $xaction) {
-          $old = $xaction->getOldValue();
-          $new = $xaction->getNewValue();
-
-          if (!strlen($new)) {
-            continue;
-          }
-
-          if ($new === $old) {
-            continue;
-          }
-
-          try {
-            PhabricatorRepository::assertValidRepositorySlug($new);
-          } catch (Exception $ex) {
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Invalid'),
-              $ex->getMessage(),
-              $xaction);
-            continue;
-          }
-
-          $other = id(new PhabricatorRepositoryQuery())
-            ->setViewer(PhabricatorUser::getOmnipotentUser())
-            ->withSlugs(array($new))
-            ->executeOne();
-          if ($other && ($other->getID() !== $object->getID())) {
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Duplicate'),
-              pht(
-                'The selected repository short name is already in use by '.
-                'another repository. Choose a unique short name.'),
-              $xaction);
-            continue;
-          }
-        }
-        break;
-
-      case PhabricatorRepositoryTransaction::TYPE_CALLSIGN:
-        foreach ($xactions as $xaction) {
-          $old = $xaction->getOldValue();
-          $new = $xaction->getNewValue();
-
-          if (!strlen($new)) {
-            continue;
-          }
-
-          if ($new === $old) {
-            continue;
-          }
-
-          try {
-            PhabricatorRepository::assertValidCallsign($new);
-          } catch (Exception $ex) {
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Invalid'),
-              $ex->getMessage(),
-              $xaction);
-            continue;
-          }
-
-          $other = id(new PhabricatorRepositoryQuery())
-            ->setViewer(PhabricatorUser::getOmnipotentUser())
-            ->withCallsigns(array($new))
-            ->executeOne();
-          if ($other && ($other->getID() !== $object->getID())) {
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Duplicate'),
-              pht(
-                'The selected callsign ("%s") is already in use by another '.
-                'repository. Choose a unique callsign.',
-                $new),
-              $xaction);
-            continue;
-          }
-        }
-        break;
-
-      case PhabricatorRepositoryTransaction::TYPE_SYMBOLS_SOURCES:
-        foreach ($xactions as $xaction) {
-          $old = $object->getSymbolSources();
-          $new = $xaction->getNewValue();
-
-          // If the viewer is adding new repositories, make sure they are
-          // valid and visible.
-          $add = array_diff($new, $old);
-          if (!$add) {
-            continue;
-          }
-
-          $repositories = id(new PhabricatorRepositoryQuery())
-            ->setViewer($this->getActor())
-            ->withPHIDs($add)
-            ->execute();
-          $repositories = mpull($repositories, null, 'getPHID');
-
-          foreach ($add as $phid) {
-            if (isset($repositories[$phid])) {
-              continue;
-            }
-
-            $errors[] = new PhabricatorApplicationTransactionValidationError(
-              $type,
-              pht('Invalid'),
-              pht(
-                'Repository ("%s") does not exist, or you do not have '.
-                'permission to see it.',
-                $phid),
-              $xaction);
-            break;
-          }
-        }
-        break;
-
-    }
-
-    return $errors;
-  }
-
   protected function didCatchDuplicateKeyException(
     PhabricatorLiskDAO $object,
     array $xactions,
diff --git a/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php b/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php
--- a/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php
+++ b/src/applications/repository/management/PhabricatorRepositoryManagementClusterizeWorkflow.php
@@ -117,7 +117,8 @@
       $xactions = array();
 
       $xactions[] = id(new PhabricatorRepositoryTransaction())
-        ->setTransactionType(PhabricatorRepositoryTransaction::TYPE_SERVICE)
+        ->setTransactionType(
+          PhabricatorRepositoryServiceTransaction::TRANSACTIONTYPE)
         ->setNewValue($service_phid);
 
       id(new PhabricatorRepositoryEditor())
diff --git a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
--- a/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
+++ b/src/applications/repository/storage/PhabricatorRepositoryTransaction.php
@@ -1,29 +1,7 @@
 <?php
 
 final class PhabricatorRepositoryTransaction
-  extends PhabricatorApplicationTransaction {
-
-  const TYPE_VCS = 'repo:vcs';
-  const TYPE_ACTIVATE = 'repo:activate';
-  const TYPE_NAME = 'repo:name';
-  const TYPE_DESCRIPTION = 'repo:description';
-  const TYPE_ENCODING = 'repo:encoding';
-  const TYPE_DEFAULT_BRANCH = 'repo:default-branch';
-  const TYPE_TRACK_ONLY = 'repo:track-only';
-  const TYPE_AUTOCLOSE_ONLY = 'repo:autoclose-only';
-  const TYPE_SVN_SUBPATH = 'repo:svn-subpath';
-  const TYPE_NOTIFY = 'repo:notify';
-  const TYPE_AUTOCLOSE = 'repo:autoclose';
-  const TYPE_PUSH_POLICY = 'repo:push-policy';
-  const TYPE_DANGEROUS = 'repo:dangerous';
-  const TYPE_ENORMOUS = 'repo:enormous';
-  const TYPE_SLUG = 'repo:slug';
-  const TYPE_SERVICE = 'repo:service';
-  const TYPE_SYMBOLS_SOURCES = 'repo:symbol-source';
-  const TYPE_SYMBOLS_LANGUAGE = 'repo:symbol-language';
-  const TYPE_STAGING_URI = 'repo:staging-uri';
-  const TYPE_AUTOMATION_BLUEPRINTS = 'repo:automation-blueprints';
-  const TYPE_CALLSIGN = 'repo:callsign';
+  extends PhabricatorModularTransaction {
 
   public function getApplicationName() {
     return 'repository';
@@ -37,381 +15,8 @@
     return null;
   }
 
-  public function getRequiredHandlePHIDs() {
-    $phids = parent::getRequiredHandlePHIDs();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_PUSH_POLICY:
-      case self::TYPE_SERVICE:
-        if ($old) {
-          $phids[] = $old;
-        }
-        if ($new) {
-          $phids[] = $new;
-        }
-        break;
-      case self::TYPE_SYMBOLS_SOURCES:
-      case self::TYPE_AUTOMATION_BLUEPRINTS:
-        if ($old) {
-          $phids = array_merge($phids, $old);
-        }
-        if ($new) {
-          $phids = array_merge($phids, $new);
-        }
-        break;
-    }
-
-    return $phids;
-  }
-
-  public function shouldHide() {
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_NAME:
-        // Hide these on create, they aren't interesting and we have an
-        // explicit "create" transaction.
-        if (!strlen($old)) {
-          return true;
-        }
-        break;
-    }
-
-    return parent::shouldHide();
-  }
-
-  public function getIcon() {
-    switch ($this->getTransactionType()) {
-      case self::TYPE_VCS:
-        return 'fa-plus';
-    }
-    return parent::getIcon();
-  }
-
-  public function getColor() {
-    switch ($this->getTransactionType()) {
-      case self::TYPE_VCS:
-        return 'green';
-    }
-    return parent::getIcon();
-  }
-
-  public function getTitle() {
-    $author_phid = $this->getAuthorPHID();
-
-    $old = $this->getOldValue();
-    $new = $this->getNewValue();
-
-    switch ($this->getTransactionType()) {
-      case self::TYPE_VCS:
-        return pht(
-          '%s created this repository.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_ACTIVATE:
-        // TODO: Old versions of this transaction use a boolean value, but
-        // should be migrated.
-        $is_deactivate =
-          (!$new) ||
-          ($new == PhabricatorRepository::STATUS_INACTIVE);
-
-        if (!$is_deactivate) {
-          return pht(
-            '%s activated this repository.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s deactivated this repository.',
-            $this->renderHandleLink($author_phid));
-        }
-      case self::TYPE_NAME:
-        return pht(
-          '%s renamed this repository from "%s" to "%s".',
-          $this->renderHandleLink($author_phid),
-          $old,
-          $new);
-      case self::TYPE_DESCRIPTION:
-        return pht(
-          '%s updated the description of this repository.',
-          $this->renderHandleLink($author_phid));
-      case self::TYPE_ENCODING:
-        if (strlen($old) && !strlen($new)) {
-          return pht(
-            '%s removed the "%s" encoding configured for this repository.',
-            $this->renderHandleLink($author_phid),
-            $old);
-        } else if (strlen($new) && !strlen($old)) {
-          return pht(
-            '%s set the encoding for this repository to "%s".',
-            $this->renderHandleLink($author_phid),
-            $new);
-        } else {
-          return pht(
-            '%s changed the repository encoding from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-      case self::TYPE_DEFAULT_BRANCH:
-        if (!strlen($new)) {
-          return pht(
-            '%s removed "%s" as the default branch.',
-            $this->renderHandleLink($author_phid),
-            $old);
-        } else if (!strlen($old)) {
-          return pht(
-            '%s set the default branch to "%s".',
-            $this->renderHandleLink($author_phid),
-            $new);
-        } else {
-          return pht(
-            '%s changed the default branch from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-        break;
-      case self::TYPE_TRACK_ONLY:
-        if (!$new) {
-          return pht(
-            '%s set this repository to track all branches.',
-            $this->renderHandleLink($author_phid));
-        } else if (!$old) {
-          return pht(
-            '%s set this repository to track branches: %s.',
-            $this->renderHandleLink($author_phid),
-            implode(', ', $new));
-        } else {
-          return pht(
-            '%s changed track branches from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            implode(', ', $old),
-            implode(', ', $new));
-        }
-        break;
-      case self::TYPE_AUTOCLOSE_ONLY:
-        if (!$new) {
-          return pht(
-            '%s set this repository to autoclose on all branches.',
-            $this->renderHandleLink($author_phid));
-        } else if (!$old) {
-          return pht(
-            '%s set this repository to autoclose on branches: %s.',
-            $this->renderHandleLink($author_phid),
-            implode(', ', $new));
-        } else {
-          return pht(
-            '%s changed autoclose branches from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            implode(', ', $old),
-            implode(', ', $new));
-        }
-        break;
-      case self::TYPE_SVN_SUBPATH:
-        if (!strlen($new)) {
-          return pht(
-            '%s removed "%s" as the Import Only path.',
-            $this->renderHandleLink($author_phid),
-            $old);
-        } else if (!strlen($old)) {
-          return pht(
-            '%s set the repository to import only "%s".',
-            $this->renderHandleLink($author_phid),
-            $new);
-        } else {
-          return pht(
-            '%s changed the import path from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-        break;
-      case self::TYPE_NOTIFY:
-        if ($new) {
-          return pht(
-            '%s enabled notifications and publishing for this repository.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s disabled notifications and publishing for this repository.',
-            $this->renderHandleLink($author_phid));
-        }
-        break;
-      case self::TYPE_AUTOCLOSE:
-        if ($new) {
-          return pht(
-            '%s enabled autoclose for this repository.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s disabled autoclose for this repository.',
-            $this->renderHandleLink($author_phid));
-        }
-        break;
-      case self::TYPE_PUSH_POLICY:
-        return pht(
-          '%s changed the push policy of this repository from "%s" to "%s".',
-          $this->renderHandleLink($author_phid),
-          $this->renderPolicyName($old, 'old'),
-          $this->renderPolicyName($new, 'new'));
-      case self::TYPE_DANGEROUS:
-        if ($new) {
-          return pht(
-            '%s disabled protection against dangerous changes.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s enabled protection against dangerous changes.',
-            $this->renderHandleLink($author_phid));
-        }
-      case self::TYPE_ENORMOUS:
-        if ($new) {
-          return pht(
-            '%s disabled protection against enormous changes.',
-            $this->renderHandleLink($author_phid));
-        } else {
-          return pht(
-            '%s enabled protection against enormous changes.',
-            $this->renderHandleLink($author_phid));
-        }
-      case self::TYPE_SLUG:
-        if (strlen($old) && !strlen($new)) {
-          return pht(
-            '%s removed the short name of this repository.',
-            $this->renderHandleLink($author_phid));
-        } else if (strlen($new) && !strlen($old)) {
-          return pht(
-            '%s set the short name of this repository to "%s".',
-            $this->renderHandleLink($author_phid),
-            $new);
-        } else {
-          return pht(
-            '%s changed the short name of this repository from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-      case self::TYPE_SERVICE:
-        if (strlen($old) && !strlen($new)) {
-          return pht(
-            '%s moved storage for this repository from %s to local.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($old));
-        } else if (!strlen($old) && strlen($new)) {
-          // TODO: Possibly, we should distinguish between automatic assignment
-          // on creation vs explicit adjustment.
-          return pht(
-            '%s set storage for this repository to %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($new));
-        } else {
-          return pht(
-            '%s moved storage for this repository from %s to %s.',
-            $this->renderHandleLink($author_phid),
-            $this->renderHandleLink($old),
-            $this->renderHandleLink($new));
-        }
-      case self::TYPE_SYMBOLS_SOURCES:
-        return pht(
-          '%s changed symbol sources from %s to %s.',
-          $this->renderHandleLink($author_phid),
-          empty($old) ? pht('None') : $this->renderHandleList($old),
-          empty($new) ? pht('None') : $this->renderHandleList($new));
-
-      case self::TYPE_SYMBOLS_LANGUAGE:
-        return pht('%s changed indexed languages from %s to %s.',
-          $this->renderHandleLink($author_phid),
-          $old ? implode(', ', $old) : pht('Any'),
-          $new ? implode(', ', $new) : pht('Any'));
-
-      case self::TYPE_STAGING_URI:
-        if (!$old) {
-          return pht(
-            '%s set "%s" as the staging area for this repository.',
-            $this->renderHandleLink($author_phid),
-            $new);
-        } else if (!$new) {
-          return pht(
-            '%s removed "%s" as the staging area for this repository.',
-            $this->renderHandleLink($author_phid),
-            $old);
-        } else {
-          return pht(
-            '%s changed the staging area for this repository from '.
-            '"%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-
-      case self::TYPE_AUTOMATION_BLUEPRINTS:
-        $add = array_diff($new, $old);
-        $rem = array_diff($old, $new);
-
-        if ($add && $rem) {
-          return pht(
-            '%s changed %s automation blueprint(s), '.
-            'added %s: %s; removed %s: %s.',
-            $this->renderHandleLink($author_phid),
-            new PhutilNumber(count($add) + count($rem)),
-            new PhutilNumber(count($add)),
-            $this->renderHandleList($add),
-            new PhutilNumber(count($rem)),
-            $this->renderHandleList($rem));
-        } else if ($add) {
-          return pht(
-            '%s added %s automation blueprint(s): %s.',
-            $this->renderHandleLink($author_phid),
-            new PhutilNumber(count($add)),
-            $this->renderHandleList($add));
-        } else {
-          return pht(
-            '%s removed %s automation blueprint(s): %s.',
-            $this->renderHandleLink($author_phid),
-            new PhutilNumber(count($rem)),
-            $this->renderHandleList($rem));
-        }
-
-      case self::TYPE_CALLSIGN:
-        if ($old === null) {
-          return pht(
-            '%s set the callsign for this repository to "%s".',
-            $this->renderHandleLink($author_phid),
-            $new);
-        } else if ($new === null) {
-          return pht(
-            '%s removed the callsign ("%s") for this repository.',
-            $this->renderHandleLink($author_phid),
-            $old);
-        } else {
-          return pht(
-            '%s changed the callsign for this repository from "%s" to "%s".',
-            $this->renderHandleLink($author_phid),
-            $old,
-            $new);
-        }
-
-    }
-
-    return parent::getTitle();
-  }
-
-  public function hasChangeDetails() {
-    switch ($this->getTransactionType()) {
-      case self::TYPE_DESCRIPTION:
-        return true;
-    }
-    return parent::hasChangeDetails();
-  }
-
-  public function renderChangeDetails(PhabricatorUser $viewer) {
-    return $this->renderTextCorpusChangeDetails(
-      $viewer,
-      $this->getOldValue(),
-      $this->getNewValue());
+  public function getBaseTransactionClass() {
+    return 'PhabricatorRepositoryTransactionType';
   }
 
 }
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryActivateTransaction.php
@@ -0,0 +1,61 @@
+<?php
+
+final class PhabricatorRepositoryActivateTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:activate';
+
+  public function generateOldValue($object) {
+    return $object->isTracked();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    // The first time a repository is activated, clear the "new repository"
+    // flag so we stop showing setup hints.
+    if ($value) {
+      $object->setDetail('newly-initialized', false);
+    }
+
+    $object->setDetail('tracking-enabled', $value);
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+
+    // TODO: Old versions of this transaction use a boolean value, but
+    // should be migrated.
+    $is_deactivate =
+      (!$new) ||
+      ($new == PhabricatorRepository::STATUS_INACTIVE);
+
+    if (!$is_deactivate) {
+      return pht(
+        '%s activated this repository.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+        '%s deactivated this repository.',
+        $this->renderAuthor());
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    $status_map = PhabricatorRepository::getStatusMap();
+    foreach ($xactions as $xaction) {
+      $status = $xaction->getNewValue();
+      if (empty($status_map[$status])) {
+        $errors[] = $this->newInvalidError(
+          pht(
+            'Repository status "%s" is not valid. Valid statuses are: %s.',
+            $status,
+            implode(', ', array_keys($status_map))),
+          $xaction);
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseOnlyTransaction.php
@@ -0,0 +1,42 @@
+<?php
+
+final class PhabricatorRepositoryAutocloseOnlyTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:autoclose-only';
+
+  public function generateOldValue($object) {
+    return array_keys($object->getDetail('close-commits-filter', array()));
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('close-commits-filter', array_fill_keys($value, true));
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (!$new) {
+      return pht(
+        '%s set this repository to autoclose on all branches.',
+        $this->renderAuthor());
+    } else if (!$old) {
+      return pht(
+        '%s set this repository to autoclose on branches: %s.',
+        $this->renderAuthor(),
+        $this->renderValue(implode(', ', $new)));
+    } else {
+      return pht(
+        '%s changed autoclose branches from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderValue(implode(', ', $old)),
+        $this->renderValue(implode(', ', $new)));
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    return $this->validateRefList($object, $xactions);
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryAutocloseTransaction.php
@@ -0,0 +1,34 @@
+<?php
+
+final class PhabricatorRepositoryAutocloseTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:autoclose';
+
+  public function generateOldValue($object) {
+    return (int)!$object->getDetail('disable-autoclose');
+  }
+
+  public function generateNewValue($object, $value) {
+    return (int)$value;
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('disable-autoclose', (int)!$value);
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+
+    if ($new) {
+      return pht(
+        '%s enabled autoclose for this repository.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+        '%s disabled autoclose for this repository.',
+        $this->renderAuthor());
+    }
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryBlueprintsTransaction.php
@@ -0,0 +1,81 @@
+<?php
+
+final class PhabricatorRepositoryBlueprintsTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:automation-blueprints';
+
+  public function generateOldValue($object) {
+    return $object->getDetail('automation.blueprintPHIDs', array());
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('automation.blueprintPHIDs', $value);
+  }
+
+  public function applyExternalEffects($object, $value) {
+    DrydockAuthorization::applyAuthorizationChanges(
+      $this->getActor(),
+      $object->getPHID(),
+      $this->getOldValue(),
+      $this->getNewValue());
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    $add = array_diff($new, $old);
+    $rem = array_diff($old, $new);
+
+    if ($add && $rem) {
+      return pht(
+        '%s changed %s automation blueprint(s), '.
+        'added %s: %s; removed %s: %s.',
+        $this->renderAuthor(),
+        new PhutilNumber(count($add) + count($rem)),
+        new PhutilNumber(count($add)),
+        $this->renderHandleList($add),
+        new PhutilNumber(count($rem)),
+        $this->renderHandleList($rem));
+    } else if ($add) {
+      return pht(
+        '%s added %s automation blueprint(s): %s.',
+        $this->renderAuthor(),
+        new PhutilNumber(count($add)),
+        $this->renderHandleList($add));
+    } else {
+      return pht(
+        '%s removed %s automation blueprint(s): %s.',
+        $this->renderAuthor(),
+        new PhutilNumber(count($rem)),
+        $this->renderHandleList($rem));
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    foreach ($xactions as $xaction) {
+      $old = nonempty($xaction->getOldValue(), array());
+      $new = nonempty($xaction->getNewValue(), array());
+
+      $add = array_diff($new, $old);
+
+      $invalid = PhabricatorObjectQuery::loadInvalidPHIDsForViewer(
+        $this->getActor(),
+        $add);
+      if ($invalid) {
+        $errors[] = $this->newInvalidError(
+          pht(
+            'Some of the selected automation blueprints are invalid '.
+            'or restricted: %s.',
+            implode(', ', $invalid)),
+          $xaction);
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryCallsignTransaction.php
@@ -0,0 +1,90 @@
+<?php
+
+final class PhabricatorRepositoryCallsignTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:callsign';
+
+  public function generateOldValue($object) {
+    return $object->getCallsign();
+  }
+
+  public function generateNewValue($object, $value) {
+    if (strlen($value)) {
+      return $value;
+    }
+
+    return null;
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setCallsign($value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (!strlen($old)) {
+      return pht(
+        '%s set the callsign for this repository to %s.',
+        $this->renderAuthor(),
+        $this->renderNewValue());
+    } else if (!strlen($new)) {
+      return pht(
+        '%s removed the callsign (%s) for this repository.',
+        $this->renderAuthor(),
+        $this->renderOldValue());
+    } else {
+      return pht(
+        '%s changed the callsign for this repository from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    foreach ($xactions as $xaction) {
+      $old = $xaction->getOldValue();
+      $new = $xaction->getNewValue();
+
+      if (!strlen($new)) {
+        continue;
+      }
+
+      if ($new === $old) {
+        continue;
+      }
+
+      try {
+        PhabricatorRepository::assertValidCallsign($new);
+      } catch (Exception $ex) {
+        $errors[] = $this->newInvalidError(
+          $ex->getMessage(),
+          $xaction);
+        continue;
+      }
+
+      $other = id(new PhabricatorRepositoryQuery())
+        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->withCallsigns(array($new))
+        ->executeOne();
+      if ($other && ($other->getID() !== $object->getID())) {
+        $errors[] = $this->newError(
+          pht('Duplicate'),
+          pht(
+            'The selected callsign ("%s") is already in use by another '.
+            'repository. Choose a unique callsign.',
+            $new),
+          $xaction);
+        continue;
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryDangerousTransaction.php
@@ -0,0 +1,30 @@
+<?php
+
+final class PhabricatorRepositoryDangerousTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:dangerous';
+
+  public function generateOldValue($object) {
+    return $object->shouldAllowDangerousChanges();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('allow-dangerous-changes', $value);
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+
+    if ($new) {
+      return pht(
+        '%s disabled protection against dangerous changes.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+        '%s enabled protection against dangerous changes.',
+        $this->renderAuthor());
+    }
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryDefaultBranchTransaction.php
@@ -0,0 +1,39 @@
+<?php
+
+final class PhabricatorRepositoryDefaultBranchTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:default-branch';
+
+  public function generateOldValue($object) {
+    return $object->getDetail('default-branch');
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('default-branch', $value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (!strlen($new)) {
+      return pht(
+        '%s removed %s as the default branch.',
+        $this->renderAuthor(),
+        $this->renderOldValue());
+    } else if (!strlen($old)) {
+      return pht(
+        '%s set the default branch to %s.',
+        $this->renderAuthor(),
+        $this->renderNewValue());
+    } else {
+      return pht(
+        '%s changed the default branch from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    }
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryDescriptionTransaction.php
@@ -0,0 +1,49 @@
+<?php
+
+final class PhabricatorRepositoryDescriptionTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:description';
+
+  public function generateOldValue($object) {
+    return $object->getDetail('description');
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('description', $value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s updated the description for this repository.',
+      $this->renderAuthor());
+  }
+
+  public function hasChangeDetailView() {
+    return true;
+  }
+
+  public function getMailDiffSectionHeader() {
+    return pht('CHANGES TO REPOSITORY DESCRIPTION');
+  }
+
+  public function newChangeDetailView() {
+    $viewer = $this->getViewer();
+
+    return id(new PhabricatorApplicationTransactionTextDiffDetailView())
+      ->setViewer($viewer)
+      ->setOldText($this->getOldValue())
+      ->setNewText($this->getNewValue());
+  }
+
+  public function newRemarkupChanges() {
+    $changes = array();
+
+    $changes[] = $this->newRemarkupChange()
+      ->setOldValue($this->getOldValue())
+      ->setNewValue($this->getNewValue());
+
+    return $changes;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryEncodingTransaction.php
@@ -0,0 +1,68 @@
+<?php
+
+final class PhabricatorRepositoryEncodingTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:encoding';
+
+  public function generateOldValue($object) {
+    return $object->getDetail('encoding');
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('encoding', $value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (strlen($old) && !strlen($new)) {
+      return pht(
+        '%s removed the %s encoding configured for this repository.',
+        $this->renderAuthor(),
+        $this->renderOldValue());
+    } else if (strlen($new) && !strlen($old)) {
+      return pht(
+        '%s set the encoding for this repository to %s.',
+        $this->renderAuthor(),
+        $this->renderNewValue());
+    } else {
+      return pht(
+        '%s changed the repository encoding from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    foreach ($xactions as $xaction) {
+      // Make sure the encoding is valid by converting to UTF-8. This tests
+      // that the user has mbstring installed, and also that they didn't
+      // type a garbage encoding name. Note that we're converting from
+      // UTF-8 to the target encoding, because mbstring is fine with
+      // converting from a nonsense encoding.
+      $encoding = $xaction->getNewValue();
+      if (!strlen($encoding)) {
+        continue;
+      }
+
+      try {
+        phutil_utf8_convert('.', $encoding, 'UTF-8');
+      } catch (Exception $ex) {
+        $errors[] = $this->newInvalidError(
+          pht(
+            'Repository encoding "%s" is not valid: %s',
+            $encoding,
+            $ex->getMessage()),
+          $xaction);
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryEnormousTransaction.php
@@ -0,0 +1,30 @@
+<?php
+
+final class PhabricatorRepositoryEnormousTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:enormous';
+
+  public function generateOldValue($object) {
+    return $object->shouldAllowEnormousChanges();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('allow-enormous-changes', $value);
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+
+    if ($new) {
+      return pht(
+        '%s disabled protection against enormous changes.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+        '%s enabled protection against enormous changes.',
+        $this->renderAuthor());
+    }
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryNameTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryNameTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryNameTransaction.php
@@ -0,0 +1,35 @@
+<?php
+
+final class PhabricatorRepositoryNameTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:name';
+
+  public function generateOldValue($object) {
+    return $object->getName();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setName($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s renamed this repository from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderOldValue(),
+      $this->renderNewValue());
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    if ($this->isEmptyTextTransaction($object->getName(), $xactions)) {
+      $errors[] = $this->newRequiredError(
+        pht('Repositories must have a name.'));
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryNotifyTransaction.php
@@ -0,0 +1,34 @@
+<?php
+
+final class PhabricatorRepositoryNotifyTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:notify';
+
+  public function generateOldValue($object) {
+    return (int)!$object->getDetail('herald-disabled');
+  }
+
+  public function generateNewValue($object, $value) {
+    return (int)$value;
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('herald-disabled', (int)!$value);
+  }
+
+  public function getTitle() {
+    $new = $this->getNewValue();
+
+    if ($new) {
+      return pht(
+        '%s enabled notifications and publishing for this repository.',
+        $this->renderAuthor());
+    } else {
+      return pht(
+        '%s disabled notifications and publishing for this repository.',
+        $this->renderAuthor());
+    }
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryPushPolicyTransaction.php
@@ -0,0 +1,24 @@
+<?php
+
+final class PhabricatorRepositoryPushPolicyTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:push-policy';
+
+  public function generateOldValue($object) {
+    return $object->getPushPolicy();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setPushPolicy($value);
+  }
+
+  public function getTitle() {
+    return pht(
+      '%s changed the push policy of this repository from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderOldPolicy(),
+      $this->renderNewPolicy());
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php b/src/applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositorySVNSubpathTransaction.php
@@ -0,0 +1,39 @@
+<?php
+
+final class PhabricatorRepositorySVNSubpathTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:svn-subpath';
+
+  public function generateOldValue($object) {
+    return $object->getDetail('svn-subpath');
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('svn-subpath', $value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (!strlen($new)) {
+      return pht(
+        '%s removed %s as the "Import Only" path.',
+        $this->renderAuthor(),
+        $this->renderOldValue());
+    } else if (!strlen($old)) {
+      return pht(
+        '%s set the repository "Import Only" path to %s.',
+        $this->renderAuthor(),
+        $this->renderNewValue());
+    } else {
+      return pht(
+        '%s changed the "Import Only" path from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    }
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryServiceTransaction.php
@@ -0,0 +1,59 @@
+<?php
+
+final class PhabricatorRepositoryServiceTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:service';
+
+  public function generateOldValue($object) {
+    return $object->getAlmanacServicePHID();
+  }
+
+  public function generateNewValue($object, $value) {
+    if (strlen($value)) {
+      return $value;
+    }
+
+    return null;
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setAlmanacServicePHID($value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getOldValue();
+
+    if (strlen($old) && !strlen($new)) {
+      return pht(
+        '%s moved storage for this repository from %s to local.',
+        $this->renderAuthor(),
+        $this->renderOldHandle());
+    } else if (!strlen($old) && strlen($new)) {
+      // TODO: Possibly, we should distinguish between automatic assignment
+      // on creation vs explicit adjustment.
+      return pht(
+        '%s set storage for this repository to %s.',
+        $this->renderAuthor(),
+        $this->renderNewHandle());
+    } else {
+      return pht(
+        '%s moved storage for this repository from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldHandle(),
+        $this->renderNewHandle());
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    // TODO: This could use some validation, values should be valid Almanac
+    // services of appropriate types. It's only reachable via the CLI so it's
+    // difficult to get wrong in practice.
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositorySlugTransaction.php b/src/applications/repository/xaction/PhabricatorRepositorySlugTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositorySlugTransaction.php
@@ -0,0 +1,88 @@
+<?php
+
+final class PhabricatorRepositorySlugTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:slug';
+
+  public function generateOldValue($object) {
+    return $object->getRepositorySlug();
+  }
+
+  public function generateNewValue($object, $value) {
+    if (strlen($value)) {
+      return $value;
+    }
+
+    return null;
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setRepositorySlug($value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (strlen($old) && !strlen($new)) {
+      return pht(
+        '%s removed the short name of this repository.',
+        $this->renderAuthor());
+    } else if (strlen($new) && !strlen($old)) {
+      return pht(
+        '%s set the short name of this repository to %s.',
+        $this->renderAuthor(),
+        $this->renderNewValue());
+    } else {
+      return pht(
+        '%s changed the short name of this repository from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    foreach ($xactions as $xaction) {
+      $old = $xaction->getOldValue();
+      $new = $xaction->getNewValue();
+
+      if (!strlen($new)) {
+        continue;
+      }
+
+      if ($new === $old) {
+        continue;
+      }
+
+      try {
+        PhabricatorRepository::assertValidRepositorySlug($new);
+      } catch (Exception $ex) {
+        $errors[] = $this->newInvalidError(
+          $ex->getMessage(),
+          $xaction);
+        continue;
+      }
+
+      $other = id(new PhabricatorRepositoryQuery())
+        ->setViewer(PhabricatorUser::getOmnipotentUser())
+        ->withSlugs(array($new))
+        ->executeOne();
+      if ($other && ($other->getID() !== $object->getID())) {
+        $errors[] = $this->newError(
+          pht('Duplicate'),
+          pht(
+            'The selected repository short name is already in use by '.
+            'another repository. Choose a unique short name.'),
+          $xaction);
+        continue;
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryStagingURITransaction.php
@@ -0,0 +1,68 @@
+<?php
+
+final class PhabricatorRepositoryStagingURITransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:staging-uri';
+
+  public function generateOldValue($object) {
+    return $object->getDetail('staging-uri');
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('staging-uri', $value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (!strlen($old)) {
+      return pht(
+        '%s set %s as the staging area for this repository.',
+        $this->renderAuthor(),
+        $this->renderNewValue());
+    } else if (!strlen($new)) {
+      return pht(
+        '%s removed %s as the staging area for this repository.',
+        $this->renderAuthor(),
+        $this->renderOldValue());
+    } else {
+      return pht(
+        '%s changed the staging area for this repository from '.
+        '%s to %s.',
+        $this->renderAuthor(),
+        $this->renderOldValue(),
+        $this->renderNewValue());
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    $old = $this->generateOldValue($object);
+    foreach ($xactions as $xaction) {
+      $new = $xaction->getNewValue();
+
+      if (!strlen($new)) {
+        continue;
+      }
+
+      if ($new === $old) {
+        continue;
+      }
+
+      try {
+        PhabricatorRepository::assertValidRemoteURI($new);
+      } catch (Exception $ex) {
+        $errors[] = $this->newInvalidError(
+          $ex->getMessage(),
+          $xaction);
+        continue;
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php b/src/applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositorySymbolLanguagesTransaction.php
@@ -0,0 +1,39 @@
+<?php
+
+final class PhabricatorRepositorySymbolLanguagesTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:symbol-language';
+
+  public function generateOldValue($object) {
+    return $object->getSymbolLanguages();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('symbol-languages', $value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if ($old) {
+      $display_old = implode(', ', $old);
+    } else {
+      $display_old = pht('Any');
+    }
+
+    if ($new) {
+      $display_new = implode(', ', $new);
+    } else {
+      $display_new = pht('Any');
+    }
+
+    return pht(
+      '%s changed indexed languages from %s to %s.',
+      $this->renderAuthor(),
+      $this->renderValue($display_old),
+      $this->renderValue($display_new));
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php b/src/applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositorySymbolSourcesTransaction.php
@@ -0,0 +1,78 @@
+<?php
+
+final class PhabricatorRepositorySymbolSourcesTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:symbol-source';
+
+  public function generateOldValue($object) {
+    return $object->getSymbolSources();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('symbol-sources', $value);
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if ($old) {
+      $display_old = $this->renderHandleList($old);
+    } else {
+      $display_old = $this->renderValue(pht('None'));
+    }
+
+    if ($new) {
+      $display_new = $this->renderHandleList($new);
+    } else {
+      $display_new = $this->renderValue(pht('None'));
+    }
+
+    return pht(
+      '%s changed symbol sources from %s to %s.',
+      $this->renderAuthor(),
+      $display_old,
+      $display_new);
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    foreach ($xactions as $xaction) {
+      $old = $object->getSymbolSources();
+      $new = $xaction->getNewValue();
+
+      // If the viewer is adding new repositories, make sure they are
+      // valid and visible.
+      $add = array_diff($new, $old);
+      if (!$add) {
+        continue;
+      }
+
+      $repositories = id(new PhabricatorRepositoryQuery())
+        ->setViewer($this->getActor())
+        ->withPHIDs($add)
+        ->execute();
+      $repositories = mpull($repositories, null, 'getPHID');
+
+      foreach ($add as $phid) {
+        if (isset($repositories[$phid])) {
+          continue;
+        }
+
+        $errors[] = $this->newInvalidError(
+          pht(
+            'Repository ("%s") does not exist, or you do not have '.
+            'permission to see it.',
+            $phid),
+          $xaction);
+        break;
+      }
+    }
+
+    return $errors;
+  }
+
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryTrackOnlyTransaction.php
@@ -0,0 +1,42 @@
+<?php
+
+final class PhabricatorRepositoryTrackOnlyTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:track-only';
+
+  public function generateOldValue($object) {
+    return array_keys($object->getDetail('branch-filter', array()));
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setDetail('branch-filter', array_fill_keys($value, true));
+  }
+
+  public function getTitle() {
+    $old = $this->getOldValue();
+    $new = $this->getNewValue();
+
+    if (!$new) {
+      return pht(
+        '%s set this repository to track all branches.',
+        $this->renderAuthor());
+    } else if (!$old) {
+      return pht(
+        '%s set this repository to track branches: %s.',
+        $this->renderAuthor(),
+        $this->renderValue(implode(', ', $new)));
+    } else {
+      return pht(
+        '%s changed tracked branches from %s to %s.',
+        $this->renderAuthor(),
+        $this->renderValue(implode(', ', $old)),
+        $this->renderValue(implode(', ', $new)));
+    }
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    return $this->validateRefList($object, $xactions);
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryTransactionType.php b/src/applications/repository/xaction/PhabricatorRepositoryTransactionType.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryTransactionType.php
@@ -0,0 +1,49 @@
+<?php
+
+abstract class PhabricatorRepositoryTransactionType
+  extends PhabricatorModularTransactionType {
+
+  protected function validateRefList($object, array $xactions) {
+    $errors = array();
+
+    foreach ($xactions as $xaction) {
+      foreach ($xaction->getNewValue() as $pattern) {
+        // Check for invalid regular expressions.
+        $regexp = PhabricatorRepository::extractBranchRegexp($pattern);
+        if ($regexp !== null) {
+          $ok = @preg_match($regexp, '');
+          if ($ok === false) {
+            $errors[] = $this->newInvalidError(
+              pht(
+                'Expression "%s" is not a valid regular expression. Note '.
+                'that you must include delimiters.',
+                $regexp),
+              $xaction);
+            continue;
+          }
+        }
+
+        // Check for formatting mistakes like `regex(...)` instead of
+        // `regexp(...)`.
+        $matches = null;
+        if (preg_match('/^([^(]+)\\(.*\\)\z/', $pattern, $matches)) {
+          switch ($matches[1]) {
+            case 'regexp':
+              break;
+            default:
+              $errors[] = $this->newInvalidError(
+                pht(
+                  'Matching function "%s(...)" is not recognized. Valid '.
+                  'functions are: regexp(...).',
+                  $matches[1]),
+                $xaction);
+              break;
+          }
+        }
+      }
+    }
+
+    return $errors;
+  }
+
+}
diff --git a/src/applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php b/src/applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php
new file mode 100644
--- /dev/null
+++ b/src/applications/repository/xaction/PhabricatorRepositoryVCSTransaction.php
@@ -0,0 +1,66 @@
+<?php
+
+final class PhabricatorRepositoryVCSTransaction
+  extends PhabricatorRepositoryTransactionType {
+
+  const TRANSACTIONTYPE = 'repo:vcs';
+
+  public function generateOldValue($object) {
+    return $object->getVersionControlSystem();
+  }
+
+  public function applyInternalEffects($object, $value) {
+    $object->setVersionControlSystem($value);
+  }
+
+  public function validateTransactions($object, array $xactions) {
+    $errors = array();
+
+    $vcs_map = PhabricatorRepositoryType::getAllRepositoryTypes();
+    $current_vcs = $object->getVersionControlSystem();
+
+    if (!$this->isNewObject()) {
+      foreach ($xactions as $xaction) {
+        if ($xaction->getNewValue() == $current_vcs) {
+          continue;
+        }
+
+        $errors[] = $this->newInvalidError(
+          pht(
+            'You can not change the version control system an existing '.
+            'repository uses. It can only be set when a repository is '.
+            'first created.'),
+          $xaction);
+      }
+
+      return $errors;
+    }
+
+    $value = $object->getVersionControlSystem();
+
+    foreach ($xactions as $xaction) {
+      $value = $xaction->getNewValue();
+
+      if (isset($vcs_map[$value])) {
+        continue;
+      }
+
+      $errors[] = $this->newInvalidError(
+        pht(
+          'Specified version control system must be a VCS '.
+          'recognized by Phabricator. Valid systems are: %s.',
+          implode(', ', array_keys($vcs_map))),
+        $xaction);
+    }
+
+    if ($value === null) {
+      $errors[] = $this->newRequiredError(
+        pht(
+          'When creating a repository, you must specify a valid '.
+          'underlying version control system. Valid systems are: %s.',
+          implode(', ', array_keys($vcs_map))));
+    }
+
+    return $errors;
+  }
+}
diff --git a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php
--- a/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php
+++ b/src/applications/transactions/controller/PhabricatorApplicationTransactionValueController.php
@@ -32,7 +32,7 @@
       case PhabricatorTransactions::TYPE_VIEW_POLICY:
       case PhabricatorTransactions::TYPE_EDIT_POLICY:
       case PhabricatorTransactions::TYPE_JOIN_POLICY:
-      case PhabricatorRepositoryTransaction::TYPE_PUSH_POLICY:
+      case PhabricatorRepositoryPushPolicyTransaction::TRANSACTIONTYPE:
         break;
       default:
         return new Aphront404Response();
diff --git a/src/applications/transactions/storage/PhabricatorModularTransactionType.php b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
--- a/src/applications/transactions/storage/PhabricatorModularTransactionType.php
+++ b/src/applications/transactions/storage/PhabricatorModularTransactionType.php
@@ -199,6 +199,35 @@
     return $this->renderHandle($this->getNewValue());
   }
 
+  final protected function renderOldPolicy() {
+    return $this->renderPolicy($this->getOldValue(), 'old');
+  }
+
+  final protected function renderNewPolicy() {
+    return $this->renderPolicy($this->getNewValue(), 'new');
+  }
+
+  final protected function renderPolicy($phid, $mode) {
+    $viewer = $this->getViewer();
+    $handles = $viewer->loadHandles(array($phid));
+
+    $policy = PhabricatorPolicy::newFromPolicyAndHandle(
+      $phid,
+      $handles[$phid]);
+
+    if ($this->isTextMode()) {
+      return $this->renderValue($policy->getFullName());
+    }
+
+    $storage = $this->getStorage();
+    if ($policy->getType() == PhabricatorPolicyType::TYPE_CUSTOM) {
+      $policy->setHref('/transactions/'.$mode.'/'.$storage->getPHID().'/');
+      $policy->setWorkflow(true);
+    }
+
+    return $this->renderValue($policy->renderDescription());
+  }
+
   final protected function renderHandleList(array $phids) {
     $viewer = $this->getViewer();
     $display = $viewer->renderHandleList($phids)