@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Rough in the new custom URI panel

Summary: Ref T10748. Ref T10366. No support for editing and no impact on the UI, but get some of the basics in place.

Test Plan: {F1223279}

Reviewers: chad

Reviewed By: chad

Maniphest Tasks: T10366, T10748

Differential Revision: https://secure.phabricator.com/D15742

+571
+4
src/__phutil_library_map__.php
··· 779 779 'DiffusionRepositoryTag' => 'applications/diffusion/data/DiffusionRepositoryTag.php', 780 780 'DiffusionRepositoryTestAutomationController' => 'applications/diffusion/controller/DiffusionRepositoryTestAutomationController.php', 781 781 'DiffusionRepositoryURIsIndexEngineExtension' => 'applications/diffusion/engineextension/DiffusionRepositoryURIsIndexEngineExtension.php', 782 + 'DiffusionRepositoryURIsManagementPanel' => 'applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php', 782 783 'DiffusionRequest' => 'applications/diffusion/request/DiffusionRequest.php', 783 784 'DiffusionResolveRefsConduitAPIMethod' => 'applications/diffusion/conduit/DiffusionResolveRefsConduitAPIMethod.php', 784 785 'DiffusionResolveUserQuery' => 'applications/diffusion/query/DiffusionResolveUserQuery.php', ··· 3216 3217 'PhabricatorRepositoryTransaction' => 'applications/repository/storage/PhabricatorRepositoryTransaction.php', 3217 3218 'PhabricatorRepositoryTransactionQuery' => 'applications/repository/query/PhabricatorRepositoryTransactionQuery.php', 3218 3219 'PhabricatorRepositoryType' => 'applications/repository/constants/PhabricatorRepositoryType.php', 3220 + 'PhabricatorRepositoryURI' => 'applications/repository/storage/PhabricatorRepositoryURI.php', 3219 3221 'PhabricatorRepositoryURIIndex' => 'applications/repository/storage/PhabricatorRepositoryURIIndex.php', 3220 3222 'PhabricatorRepositoryURINormalizer' => 'applications/repository/data/PhabricatorRepositoryURINormalizer.php', 3221 3223 'PhabricatorRepositoryURINormalizerTestCase' => 'applications/repository/data/__tests__/PhabricatorRepositoryURINormalizerTestCase.php', ··· 4978 4980 'DiffusionRepositoryTag' => 'Phobject', 4979 4981 'DiffusionRepositoryTestAutomationController' => 'DiffusionRepositoryEditController', 4980 4982 'DiffusionRepositoryURIsIndexEngineExtension' => 'PhabricatorIndexEngineExtension', 4983 + 'DiffusionRepositoryURIsManagementPanel' => 'DiffusionRepositoryManagementPanel', 4981 4984 'DiffusionRequest' => 'Phobject', 4982 4985 'DiffusionResolveRefsConduitAPIMethod' => 'DiffusionQueryConduitAPIMethod', 4983 4986 'DiffusionResolveUserQuery' => 'Phobject', ··· 7875 7878 'PhabricatorRepositoryTransaction' => 'PhabricatorApplicationTransaction', 7876 7879 'PhabricatorRepositoryTransactionQuery' => 'PhabricatorApplicationTransactionQuery', 7877 7880 'PhabricatorRepositoryType' => 'Phobject', 7881 + 'PhabricatorRepositoryURI' => 'PhabricatorRepositoryDAO', 7878 7882 'PhabricatorRepositoryURIIndex' => 'PhabricatorRepositoryDAO', 7879 7883 'PhabricatorRepositoryURINormalizer' => 'Phobject', 7880 7884 'PhabricatorRepositoryURINormalizerTestCase' => 'PhabricatorTestCase',
+126
src/applications/diffusion/management/DiffusionRepositoryURIsManagementPanel.php
··· 1 + <?php 2 + 3 + final class DiffusionRepositoryURIsManagementPanel 4 + extends DiffusionRepositoryManagementPanel { 5 + 6 + const PANELKEY = 'uris'; 7 + 8 + public function getManagementPanelLabel() { 9 + return pht('Clone / Fetch / Mirror'); 10 + } 11 + 12 + public function getManagementPanelOrder() { 13 + return 300; 14 + } 15 + 16 + public function buildManagementPanelContent() { 17 + $repository = $this->getRepository(); 18 + $viewer = $this->getViewer(); 19 + 20 + $repository->attachURIs(array()); 21 + $uris = $repository->getURIs(); 22 + 23 + Javelin::initBehavior('phabricator-tooltips'); 24 + $rows = array(); 25 + foreach ($uris as $uri) { 26 + 27 + $uri_name = $uri->getDisplayURI(); 28 + 29 + if ($uri->getIsDisabled()) { 30 + $status_icon = 'fa-times grey'; 31 + } else { 32 + $status_icon = 'fa-check green'; 33 + } 34 + 35 + $uri_status = id(new PHUIIconView())->setIcon($status_icon); 36 + 37 + switch ($uri->getEffectiveIOType()) { 38 + case PhabricatorRepositoryURI::IO_OBSERVE: 39 + $io_icon = 'fa-download green'; 40 + $io_label = pht('Observe'); 41 + break; 42 + case PhabricatorRepositoryURI::IO_MIRROR: 43 + $io_icon = 'fa-upload green'; 44 + $io_label = pht('Mirror'); 45 + break; 46 + case PhabricatorRepositoryURI::IO_NONE: 47 + $io_icon = 'fa-times grey'; 48 + $io_label = pht('No I/O'); 49 + break; 50 + case PhabricatorRepositoryURI::IO_READ: 51 + $io_icon = 'fa-folder blue'; 52 + $io_label = pht('Read Only'); 53 + break; 54 + case PhabricatorRepositoryURI::IO_READWRITE: 55 + $io_icon = 'fa-folder-open blue'; 56 + $io_label = pht('Read/Write'); 57 + break; 58 + } 59 + 60 + $uri_io = array( 61 + id(new PHUIIconView())->setIcon($io_icon), 62 + ' ', 63 + $io_label, 64 + ); 65 + 66 + switch ($uri->getEffectiveDisplayType()) { 67 + case PhabricatorRepositoryURI::DISPLAY_NEVER: 68 + $display_icon = 'fa-eye-slash grey'; 69 + $display_label = pht('Hidden'); 70 + break; 71 + case PhabricatorRepositoryURI::DISPLAY_ALWAYS: 72 + $display_icon = 'fa-eye green'; 73 + $display_label = pht('Visible'); 74 + break; 75 + } 76 + 77 + $uri_display = array( 78 + id(new PHUIIconView())->setIcon($display_icon), 79 + ' ', 80 + $display_label, 81 + ); 82 + 83 + $rows[] = array( 84 + $uri_status, 85 + $uri_name, 86 + $uri_io, 87 + $uri_display, 88 + ); 89 + } 90 + 91 + $table = id(new AphrontTableView($rows)) 92 + ->setNoDataString(pht('This repository has no URIs.')) 93 + ->setHeaders( 94 + array( 95 + null, 96 + pht('URI'), 97 + pht('I/O'), 98 + pht('Display'), 99 + )) 100 + ->setColumnClasses( 101 + array( 102 + null, 103 + 'pri wide', 104 + null, 105 + null, 106 + )); 107 + 108 + $doc_href = PhabricatorEnv::getDoclink( 109 + 'Diffusion User Guide: Repository URIs'); 110 + 111 + $header = id(new PHUIHeaderView()) 112 + ->setHeader(pht('Repository URIs')) 113 + ->addActionLink( 114 + id(new PHUIButtonView()) 115 + ->setIcon('fa-book') 116 + ->setHref($doc_href) 117 + ->setTag('a') 118 + ->setText(pht('Documentation'))); 119 + 120 + return id(new PHUIObjectBoxView()) 121 + ->setHeader($header) 122 + ->setBackground(PHUIObjectBoxView::BLUE_PROPERTY) 123 + ->setTable($table); 124 + } 125 + 126 + }
+94
src/applications/repository/storage/PhabricatorRepository.php
··· 63 63 private $commitCount = self::ATTACHABLE; 64 64 private $mostRecentCommit = self::ATTACHABLE; 65 65 private $projectPHIDs = self::ATTACHABLE; 66 + private $uris = self::ATTACHABLE; 66 67 67 68 private $clusterWriteLock; 68 69 private $clusterWriteVersion; 70 + 69 71 70 72 public static function initializeNewRepository(PhabricatorUser $actor) { 71 73 $app = id(new PhabricatorApplicationQuery()) ··· 2264 2266 } 2265 2267 2266 2268 return $client; 2269 + } 2270 + 2271 + /* -( Repository URIs )---------------------------------------------------- */ 2272 + 2273 + 2274 + public function attachURIs(array $uris) { 2275 + $custom_map = array(); 2276 + foreach ($uris as $key => $uri) { 2277 + $builtin_key = $uri->getRepositoryURIBuiltinKey(); 2278 + if ($builtin_key !== null) { 2279 + $custom_map[$builtin_key] = $key; 2280 + } 2281 + } 2282 + 2283 + $builtin_uris = $this->newBuiltinURIs(); 2284 + $seen_builtins = array(); 2285 + foreach ($builtin_uris as $builtin_uri) { 2286 + $builtin_key = $builtin_uri->getRepositoryURIBuiltinKey(); 2287 + $seen_builtins[$builtin_key] = true; 2288 + 2289 + // If this builtin URI is disabled, don't attach it and remove the 2290 + // persisted version if it exists. 2291 + if ($builtin_uri->getIsDisabled()) { 2292 + if (isset($custom_map[$builtin_key])) { 2293 + unset($uris[$custom_map[$builtin_key]]); 2294 + } 2295 + continue; 2296 + } 2297 + 2298 + // If we don't have a persisted version of the URI, add the builtin 2299 + // version. 2300 + if (empty($custom_map[$builtin_key])) { 2301 + $uris[] = $builtin_uri; 2302 + } 2303 + } 2304 + 2305 + // Remove any builtins which no longer exist. 2306 + foreach ($custom_map as $builtin_key => $key) { 2307 + if (empty($seen_builtins[$builtin_key])) { 2308 + unset($uris[$key]); 2309 + } 2310 + } 2311 + 2312 + $this->uris = $uris; 2313 + 2314 + return $this; 2315 + } 2316 + 2317 + public function getURIs() { 2318 + return $this->assertAttached($this->uris); 2319 + } 2320 + 2321 + protected function newBuiltinURIs() { 2322 + $has_callsign = ($this->getCallsign() !== null); 2323 + $has_shortname = ($this->getRepositorySlug() !== null); 2324 + 2325 + $identifier_map = array( 2326 + PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_CALLSIGN => $has_callsign, 2327 + PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_SHORTNAME => $has_shortname, 2328 + PhabricatorRepositoryURI::BUILTIN_IDENTIFIER_ID => true, 2329 + ); 2330 + 2331 + $allow_http = PhabricatorEnv::getEnvConfig('diffusion.allow-http-auth'); 2332 + 2333 + $base_uri = PhabricatorEnv::getURI('/'); 2334 + $base_uri = new PhutilURI($base_uri); 2335 + $has_https = ($base_uri->getProtocol() == 'https'); 2336 + $has_https = ($has_https && $allow_http); 2337 + 2338 + $has_http = !PhabricatorEnv::getEnvConfig('security.require-https'); 2339 + $has_http = ($has_http && $allow_http); 2340 + 2341 + // TODO: Maybe allow users to disable this by default somehow? 2342 + $has_ssh = true; 2343 + 2344 + $protocol_map = array( 2345 + PhabricatorRepositoryURI::BUILTIN_PROTOCOL_SSH => $has_ssh, 2346 + PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTPS => $has_https, 2347 + PhabricatorRepositoryURI::BUILTIN_PROTOCOL_HTTP => $has_http, 2348 + ); 2349 + 2350 + $uris = array(); 2351 + foreach ($protocol_map as $protocol => $proto_supported) { 2352 + foreach ($identifier_map as $identifier => $id_supported) { 2353 + $uris[] = PhabricatorRepositoryURI::initializeNewURI($this) 2354 + ->setBuiltinProtocol($protocol) 2355 + ->setBuiltinIdentifier($identifier) 2356 + ->setIsDisabled(!$proto_supported || !$id_supported); 2357 + } 2358 + } 2359 + 2360 + return $uris; 2267 2361 } 2268 2362 2269 2363
+300
src/applications/repository/storage/PhabricatorRepositoryURI.php
··· 1 + <?php 2 + 3 + final class PhabricatorRepositoryURI 4 + extends PhabricatorRepositoryDAO { 5 + 6 + protected $repositoryPHID; 7 + protected $uri; 8 + protected $builtinProtocol; 9 + protected $builtinIdentifier; 10 + protected $credentialPHID; 11 + protected $ioType; 12 + protected $displayType; 13 + protected $isDisabled; 14 + 15 + private $repository = self::ATTACHABLE; 16 + 17 + const BUILTIN_PROTOCOL_SSH = 'ssh'; 18 + const BUILTIN_PROTOCOL_HTTP = 'http'; 19 + const BUILTIN_PROTOCOL_HTTPS = 'https'; 20 + 21 + const BUILTIN_IDENTIFIER_ID = 'id'; 22 + const BUILTIN_IDENTIFIER_SHORTNAME = 'shortname'; 23 + const BUILTIN_IDENTIFIER_CALLSIGN = 'callsign'; 24 + 25 + const DISPLAY_DEFAULT = 'default'; 26 + const DISPLAY_NEVER = 'never'; 27 + const DISPLAY_ALWAYS = 'always'; 28 + 29 + const IO_DEFAULT = 'default'; 30 + const IO_OBSERVE = 'observe'; 31 + const IO_MIRROR = 'mirror'; 32 + const IO_NONE = 'none'; 33 + const IO_READ = 'read'; 34 + const IO_READWRITE = 'readwrite'; 35 + 36 + protected function getConfiguration() { 37 + return array( 38 + self::CONFIG_AUX_PHID => true, 39 + self::CONFIG_COLUMN_SCHEMA => array( 40 + 'uri' => 'text', 41 + 'builtinProtocol' => 'text32?', 42 + 'builtinIdentifier' => 'text32?', 43 + 'ioType' => 'text32', 44 + 'displayType' => 'text32', 45 + 'isDisabled' => 'bool', 46 + ), 47 + self::CONFIG_KEY_SCHEMA => array( 48 + 'key_builtin' => array( 49 + 'columns' => array( 50 + 'repositoryPHID', 51 + 'builtinProtocol', 52 + 'builtinIdentifier', 53 + ), 54 + 'unique' => true, 55 + ), 56 + ), 57 + ) + parent::getConfiguration(); 58 + } 59 + 60 + public static function initializeNewURI(PhabricatorRepository $repository) { 61 + return id(new self()) 62 + ->attachRepository($repository) 63 + ->setRepositoryPHID($repository->getPHID()) 64 + ->setIoType(self::IO_DEFAULT) 65 + ->setDisplayType(self::DISPLAY_DEFAULT) 66 + ->setIsDisabled(0); 67 + } 68 + 69 + public function attachRepository(PhabricatorRepository $repository) { 70 + $this->repository = $repository; 71 + return $this; 72 + } 73 + 74 + public function getRepository() { 75 + return $this->assertAttached($this->repository); 76 + } 77 + 78 + public function getRepositoryURIBuiltinKey() { 79 + if (!$this->getBuiltinProtocol()) { 80 + return null; 81 + } 82 + 83 + $parts = array( 84 + $this->getBuiltinProtocol(), 85 + $this->getBuiltinIdentifier(), 86 + ); 87 + return implode('.', $parts); 88 + } 89 + 90 + public function isBuiltin() { 91 + return (bool)$this->getBuiltinProtocol(); 92 + } 93 + 94 + public function getEffectiveDisplayType() { 95 + $display = $this->getDisplayType(); 96 + 97 + if ($display != self::IO_DEFAULT) { 98 + return $display; 99 + } 100 + 101 + switch ($this->getEffectiveIOType()) { 102 + case self::IO_MIRROR: 103 + case self::IO_OBSERVE: 104 + return self::DISPLAY_NEVER; 105 + case self::IO_NONE: 106 + if ($this->isBuiltin()) { 107 + return self::DISPLAY_NEVER; 108 + } else { 109 + return self::DISPLAY_ALWAYS; 110 + } 111 + case self::IO_READ: 112 + case self::IO_READWRITE: 113 + // By default, only show the "best" version of the builtin URI, not the 114 + // other redundant versions. 115 + if ($this->isBuiltin()) { 116 + $repository = $this->getRepository(); 117 + $other_uris = $repository->getURIs(); 118 + 119 + $identifier_value = array( 120 + self::BUILTIN_IDENTIFIER_CALLSIGN => 3, 121 + self::BUILTIN_IDENTIFIER_SHORTNAME => 2, 122 + self::BUILTIN_IDENTIFIER_ID => 1, 123 + ); 124 + 125 + $have_identifiers = array(); 126 + foreach ($other_uris as $other_uri) { 127 + if ($other_uri->getIsDisabled()) { 128 + continue; 129 + } 130 + 131 + $identifier = $other_uri->getBuiltinIdentifier(); 132 + if (!$identifier) { 133 + continue; 134 + } 135 + 136 + $have_identifiers[$identifier] = $identifier_value[$identifier]; 137 + } 138 + 139 + $best_identifier = max($have_identifiers); 140 + $this_identifier = $identifier_value[$this->getBuiltinIdentifier()]; 141 + 142 + if ($this_identifier < $best_identifier) { 143 + return self::DISPLAY_NEVER; 144 + } 145 + } 146 + 147 + return self::DISPLAY_ALWAYS; 148 + } 149 + } 150 + 151 + 152 + public function getEffectiveIOType() { 153 + $io = $this->getIoType(); 154 + 155 + if ($io != self::IO_DEFAULT) { 156 + return $io; 157 + } 158 + 159 + if ($this->isBuiltin()) { 160 + $repository = $this->getRepository(); 161 + $other_uris = $repository->getURIs(); 162 + 163 + $any_observe = false; 164 + foreach ($other_uris as $other_uri) { 165 + if ($other_uri->getIoType() == self::IO_OBSERVE) { 166 + $any_observe = true; 167 + break; 168 + } 169 + } 170 + 171 + if ($any_observe) { 172 + return self::IO_READ; 173 + } else { 174 + return self::IO_READWRITE; 175 + } 176 + } 177 + 178 + return self::IO_IGNORE; 179 + } 180 + 181 + 182 + public function getDisplayURI() { 183 + $uri = new PhutilURI($this->getURI()); 184 + 185 + $protocol = $this->getForcedProtocol(); 186 + if ($protocol) { 187 + $uri->setProtocol($protocol); 188 + } 189 + 190 + $user = $this->getForcedUser(); 191 + if ($user) { 192 + $uri->setUser($user); 193 + } 194 + 195 + $host = $this->getForcedHost(); 196 + if ($host) { 197 + $uri->setDomain($host); 198 + } 199 + 200 + $port = $this->getForcedPort(); 201 + if ($port) { 202 + $uri->setPort($port); 203 + } 204 + 205 + $path = $this->getForcedPath(); 206 + if ($path) { 207 + $uri->setPath($path); 208 + } 209 + 210 + return $uri; 211 + } 212 + 213 + private function getForcedProtocol() { 214 + switch ($this->getBuiltinProtocol()) { 215 + case self::BUILTIN_PROTOCOL_SSH: 216 + return 'ssh'; 217 + case self::BUILTIN_PROTOCOL_HTTP: 218 + return 'http'; 219 + case self::BUILTIN_PROTOCOL_HTTPS: 220 + return 'https'; 221 + default: 222 + return null; 223 + } 224 + } 225 + 226 + private function getForcedUser() { 227 + switch ($this->getBuiltinProtocol()) { 228 + case self::BUILTIN_PROTOCOL_SSH: 229 + return PhabricatorEnv::getEnvConfig('diffusion.ssh-user'); 230 + default: 231 + return null; 232 + } 233 + } 234 + 235 + private function getForcedHost() { 236 + $phabricator_uri = PhabricatorEnv::getURI('/'); 237 + $phabricator_uri = new PhutilURI($phabricator_uri); 238 + 239 + $phabricator_host = $phabricator_uri->getDomain(); 240 + 241 + switch ($this->getBuiltinProtocol()) { 242 + case self::BUILTIN_PROTOCOL_SSH: 243 + $ssh_host = PhabricatorEnv::getEnvConfig('diffusion.ssh-host'); 244 + if ($ssh_host !== null) { 245 + return $ssh_host; 246 + } 247 + return $phabricator_host; 248 + case self::BUILTIN_PROTOCOL_HTTP: 249 + case self::BUILTIN_PROTOCOL_HTTPS: 250 + return $phabricator_host; 251 + default: 252 + return null; 253 + } 254 + } 255 + 256 + private function getForcedPort() { 257 + switch ($this->getBuiltinProtocol()) { 258 + case self::BUILTIN_PROTOCOL_SSH: 259 + return PhabricatorEnv::getEnvConfig('diffusion.ssh-port'); 260 + case self::BUILTIN_PROTOCOL_HTTP: 261 + case self::BUILTIN_PROTOCOL_HTTPS: 262 + default: 263 + return null; 264 + } 265 + } 266 + 267 + private function getForcedPath() { 268 + if (!$this->isBuiltin()) { 269 + return null; 270 + } 271 + 272 + $repository = $this->getRepository(); 273 + 274 + $id = $repository->getID(); 275 + $callsign = $repository->getCallsign(); 276 + $short_name = $repository->getRepositorySlug(); 277 + 278 + $clone_name = $repository->getCloneName(); 279 + 280 + if ($repository->isGit()) { 281 + $suffix = '.git'; 282 + } else if ($repository->isHg()) { 283 + $suffix = '/'; 284 + } else { 285 + $suffix = ''; 286 + } 287 + 288 + switch ($this->getBuiltinIdentifier()) { 289 + case self::BUILTIN_IDENTIFIER_ID: 290 + return "/diffusion/{$id}/{$clone_name}{$suffix}"; 291 + case self::BUILTIN_IDENTIFIER_SHORTNAME: 292 + return "/source/{$short_name}{$suffix}"; 293 + case self::BUILTIN_IDENTIFIER_CALLSIGN: 294 + return "/diffusion/{$callsign}/{$clone_name}{$suffix}"; 295 + default: 296 + return null; 297 + } 298 + } 299 + 300 + }
+47
src/docs/user/userguide/diffusion_uris.diviner
··· 1 + @title Diffusion User Guide: URIs 2 + @group userguide 3 + 4 + Guide to configuring repository URIs for fetching, cloning and mirroring. 5 + 6 + Overview 7 + ======== 8 + 9 + WARNING: This document describes a feature which is still under development, 10 + and is not necessarily accurate or complete. 11 + 12 + Phabricator can host, observe, mirror, and proxy repositories. For example, 13 + here are some supported use cases: 14 + 15 + **Host Repositories**: Phabricator can host repositories locally. Phabricator 16 + maintains the writable master version of the repository, and you can push and 17 + pull the repository. This is the most straightforward kind of repository 18 + configuration, and similar to repositories on other services like GitHub or 19 + Bitbucket. 20 + 21 + **Observe Repositories**: Phabricator can create a copy of an repository which 22 + is hosted elsewhere (like GitHub or Bitbucket) and track updates to the remote 23 + repository. This will create a read-only copy of the repository in Phabricator. 24 + 25 + **Mirror Repositories**: Phabricator can publish any repository to mirrors, 26 + updating the mirrors as changes are made to the repository. This works with 27 + both local hosted repositories and remote repositories that Phabricator is 28 + observing. 29 + 30 + **Proxy Repositories**: If you are observing a repository, you can allow users 31 + to read Phabricator's copy of the repository. Phabricator supports granular 32 + read permissions, so this can let you open a private repository up a little 33 + bit in a flexible way. 34 + 35 + **Import Repositories**: If you have a repository elsewhere that you want to 36 + host on Phabricator, you can observe the remote repository first, then turn 37 + the tracking off once the repository fully synchronizes. This allows you to 38 + copy an existing repository and begin hosting it in Phabricator. 39 + 40 + You can also import repositories by creating an empty hosted repository and 41 + then pushing everything to the repository directly. 42 + 43 + You configure the behavior of a Phabricator repository by adding and 44 + configuring URIs and marking them to be fetched from, mirrored to, clonable, 45 + and so on. By configuring all the URIs that a repository should interact with 46 + and expose to users, you configure the read, write, and mirroring behavior 47 + of the repository.