@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

Simplify Repository remote and local command construction

Summary:
This cleans up some garbage:

- We were specifying environmental variables with `X=y git ...`, but now have `setEnv()` on both `ExecFuture` and `PhutilExecPassthru`. Use `setEnv()`.
- We were specifying the working directory with `(cd %s && git ...)`, but now have `setCWD()` on both `ExecFuture` and `PhutilExecPassthru`. Use `setCWD()`.
- We were specifying the Git credentials with `ssh-agent -c (ssh-add ... && git ...)`. We can do this more cleanly with `GIT_SSH`. Use `GIT_SSH`.
- Since we have to write a script for `GIT_SSH` anyway, use the same script for Subversion and Mercurial.

This fixes two specific issues:

- Previously, we were not able to set `-o StrictHostKeyChecking=no` on Git commands, so the first time you cloned a git repo the daemons would generally prompt you to add `github.com` or whatever to `known_hosts`. Since this was non-interactive, things would mysteriously hang, in effect. With `GIT_SSH`, we can specify the flag, reducing the number of ways things can go wrong.
- This adds `LANG=C`, which probably (?) forces the language to English for all commands. Apparently you need to install special language packs or something, so I don't know that this actually works, but at least two users with non-English languages have claimed it does (see <https://github.com/facebook/arcanist/pull/114> for a similar issue in Arcanist).

At some point in the future I might want to combine the Arcanist code for command execution with the Phabricator code for command execution (they share some stuff like LANG and HGPLAIN). However, credential management is kind of messy, so I'm adopting a "wait and see" approach for now. I expect to split this at least somewhat in the future, for Drydock/Automerge if nothing else.

Also I'm not sure if we use the passthru stuff at all anymore, I may just be able to delete that. I'll check in a future diff.

Test Plan: Browsed and pulled Git, Subversion and Mercurial repositories.

Reviewers: btrahan

Reviewed By: btrahan

CC: aran

Maniphest Tasks: T2230

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

+238 -137
+1
bin/ssh-connect
··· 1 + ../scripts/ssh/ssh-connect.php
+71
scripts/ssh/ssh-connect.php
··· 1 + #!/usr/bin/env php 2 + <?php 3 + 4 + // This is a wrapper script for Git, Mercurial, and Subversion. It primarily 5 + // serves to inject "-o StrictHostKeyChecking=no" into the SSH arguments. 6 + 7 + $root = dirname(dirname(dirname(__FILE__))); 8 + require_once $root.'/scripts/__init_script__.php'; 9 + 10 + $target_name = getenv('PHABRICATOR_SSH_TARGET'); 11 + if (!$target_name) { 12 + throw new Exception(pht("No 'PHABRICATOR_SSH_TARGET' in environment!")); 13 + } 14 + 15 + $repository = id(new PhabricatorRepositoryQuery()) 16 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 17 + ->withCallsigns(array($target_name)) 18 + ->executeOne(); 19 + if (!$repository) { 20 + throw new Exception(pht('No repository with callsign "%s"!', $target_name)); 21 + } 22 + 23 + $pattern = array(); 24 + $arguments = array(); 25 + 26 + $pattern[] = 'ssh'; 27 + 28 + $pattern[] = '-o'; 29 + $pattern[] = 'StrictHostKeyChecking=no'; 30 + 31 + $login = $repository->getSSHLogin(); 32 + if (strlen($login)) { 33 + $pattern[] = '-l'; 34 + $pattern[] = '%P'; 35 + $arguments[] = new PhutilOpaqueEnvelope($login); 36 + } 37 + 38 + $ssh_identity = null; 39 + 40 + $key = $repository->getDetail('ssh-key'); 41 + $keyfile = $repository->getDetail('ssh-keyfile'); 42 + if ($keyfile) { 43 + $ssh_identity = $keyfile; 44 + } else if ($key) { 45 + $tmpfile = new TempFile('phabricator-repository-ssh-key'); 46 + chmod($tmpfile, 0600); 47 + Filesystem::writeFile($tmpfile, $key); 48 + $ssh_identity = (string)$tmpfile; 49 + } 50 + 51 + if ($ssh_identity) { 52 + $pattern[] = '-i'; 53 + $pattern[] = '%P'; 54 + $arguments[] = new PhutilOpaqueEnvelope($keyfile); 55 + } 56 + 57 + $pattern[] = '--'; 58 + 59 + $passthru_args = array_slice($argv, 1); 60 + foreach ($passthru_args as $passthru_arg) { 61 + $pattern[] = '%s'; 62 + $arguments[] = $passthru_arg; 63 + } 64 + 65 + $pattern = implode(' ', $pattern); 66 + array_unshift($arguments, $pattern); 67 + 68 + $err = newv('PhutilExecPassthru', $arguments) 69 + ->execute(); 70 + 71 + exit($err);
+166 -137
src/applications/repository/storage/PhabricatorRepository.php
··· 194 194 return $uri; 195 195 } 196 196 197 + 198 + /* -( Remote Command Execution )------------------------------------------- */ 199 + 200 + 197 201 public function execRemoteCommand($pattern /* , $arg, ... */) { 198 202 $args = func_get_args(); 199 - $args = $this->formatRemoteCommand($args); 200 - return call_user_func_array('exec_manual', $args); 203 + return $this->newRemoteCommandFuture($args)->resolve(); 201 204 } 202 205 203 206 public function execxRemoteCommand($pattern /* , $arg, ... */) { 204 207 $args = func_get_args(); 205 - $args = $this->formatRemoteCommand($args); 206 - return call_user_func_array('execx', $args); 208 + return $this->newRemoteCommandFuture($args)->resolvex(); 207 209 } 208 210 209 211 public function getRemoteCommandFuture($pattern /* , $arg, ... */) { 210 212 $args = func_get_args(); 211 - $args = $this->formatRemoteCommand($args); 212 - return newv('ExecFuture', $args); 213 + return $this->newRemoteCommandFuture($args); 213 214 } 214 215 215 216 public function passthruRemoteCommand($pattern /* , $arg, ... */) { 216 217 $args = func_get_args(); 217 - $args = $this->formatRemoteCommand($args); 218 - return call_user_func_array('phutil_passthru', $args); 218 + return $this->newRemoteCommandPassthru($args)->execute(); 219 219 } 220 220 221 - public function execLocalCommand($pattern /* , $arg, ... */) { 222 - $this->assertLocalExists(); 221 + private function newRemoteCommandFuture(array $argv) { 222 + $argv = $this->formatRemoteCommand($argv); 223 + $future = newv('ExecFuture', $argv); 224 + $future->setEnv($this->getRemoteCommandEnvironment()); 225 + return $future; 226 + } 223 227 228 + private function newRemoteCommandPassthru(array $argv) { 229 + $argv = $this->formatRemoteCommand($argv); 230 + $passthru = newv('PhutilExecPassthru', $argv); 231 + $passthru->setEnv($this->getRemoteCommandEnvironment()); 232 + return $passthru; 233 + } 234 + 235 + 236 + /* -( Local Command Execution )-------------------------------------------- */ 237 + 238 + 239 + public function execLocalCommand($pattern /* , $arg, ... */) { 224 240 $args = func_get_args(); 225 - $args = $this->formatLocalCommand($args); 226 - return call_user_func_array('exec_manual', $args); 241 + return $this->newLocalCommandFuture($args)->resolve(); 227 242 } 228 243 229 244 public function execxLocalCommand($pattern /* , $arg, ... */) { 230 - $this->assertLocalExists(); 231 - 232 245 $args = func_get_args(); 233 - $args = $this->formatLocalCommand($args); 234 - return call_user_func_array('execx', $args); 246 + return $this->newLocalCommandFuture($args)->resolvex(); 235 247 } 236 248 237 249 public function getLocalCommandFuture($pattern /* , $arg, ... */) { 238 - $this->assertLocalExists(); 250 + $args = func_get_args(); 251 + return $this->newLocalCommandFuture($args); 252 + } 239 253 254 + public function passthruLocalCommand($pattern /* , $arg, ... */) { 240 255 $args = func_get_args(); 241 - $args = $this->formatLocalCommand($args); 242 - return newv('ExecFuture', $args); 256 + return $this->newLocalCommandPassthru($args)->execute(); 243 257 } 244 258 245 - public function passthruLocalCommand($pattern /* , $arg, ... */) { 259 + private function newLocalCommandFuture(array $argv) { 246 260 $this->assertLocalExists(); 247 261 248 - $args = func_get_args(); 249 - $args = $this->formatLocalCommand($args); 250 - return call_user_func_array('phutil_passthru', $args); 262 + $argv = $this->formatLocalCommand($argv); 263 + $future = newv('ExecFuture', $argv); 264 + $future->setEnv($this->getLocalCommandEnvironment()); 265 + 266 + if ($this->usesLocalWorkingCopy()) { 267 + $future->setCWD($this->getLocalPath()); 268 + } 269 + 270 + return $future; 251 271 } 252 272 273 + private function newLocalCommandPassthru(array $argv) { 274 + $this->assertLocalExists(); 253 275 254 - private function formatRemoteCommand(array $args) { 255 - $pattern = $args[0]; 256 - $args = array_slice($args, 1); 276 + $argv = $this->formatLocalCommand($argv); 277 + $future = newv('PhutilExecPassthru', $argv); 278 + $future->setEnv($this->getLocalCommandEnvironment()); 257 279 258 - $empty = $this->getEmptyReadableDirectoryPath(); 280 + if ($this->usesLocalWorkingCopy()) { 281 + $future->setCWD($this->getLocalPath()); 282 + } 283 + 284 + return $future; 285 + } 286 + 287 + 288 + /* -( Command Infrastructure )--------------------------------------------- */ 289 + 290 + 291 + private function getSSHWrapper() { 292 + $root = dirname(phutil_get_library_root('phabricator')); 293 + return $root.'/bin/ssh-connect'; 294 + } 295 + 296 + private function getCommonCommandEnvironment() { 297 + $env = array( 298 + // NOTE: Force the language to "C", which overrides locale settings. 299 + // This makes stuff print in English instead of, e.g., French, so we can 300 + // parse the output of some commands, error messages, etc. 301 + 'LANG' => 'C', 302 + ); 303 + 304 + switch ($this->getVersionControlSystem()) { 305 + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 306 + break; 307 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 308 + // NOTE: See T2965. Some time after Git 1.7.5.4, Git started fataling if 309 + // it can not read $HOME. For many users, $HOME points at /root (this 310 + // seems to be a default result of Apache setup). Instead, explicitly 311 + // point $HOME at a readable, empty directory so that Git looks for the 312 + // config file it's after, fails to locate it, and moves on. This is 313 + // really silly, but seems like the least damaging approach to 314 + // mitigating the issue. 315 + 316 + $root = dirname(phutil_get_library_root('phabricator')); 317 + $env['HOME'] = $root.'/support/empty/'; 318 + break; 319 + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 320 + // NOTE: This overrides certain configuration, extensions, and settings 321 + // which make Mercurial commands do random unusual things. 322 + $env['HGPLAIN'] = 1; 323 + break; 324 + default: 325 + throw new Exception("Unrecognized version control system."); 326 + } 327 + 328 + return $env; 329 + } 330 + 331 + private function getLocalCommandEnvironment() { 332 + return $this->getCommonCommandEnvironment(); 333 + } 334 + 335 + private function getRemoteCommandEnvironment() { 336 + $env = $this->getCommonCommandEnvironment(); 259 337 260 338 if ($this->shouldUseSSH()) { 339 + // NOTE: This is read by `bin/ssh-connect`, and tells it which credentials 340 + // to use. 341 + $env['PHABRICATOR_SSH_TARGET'] = $this->getCallsign(); 261 342 switch ($this->getVersionControlSystem()) { 262 343 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 263 - $pattern = "SVN_SSH=%s svn --non-interactive {$pattern}"; 264 - array_unshift( 265 - $args, 266 - csprintf( 267 - 'ssh -l %P -i %P', 268 - new PhutilOpaqueEnvelope($this->getSSHLogin()), 269 - new PhutilOpaqueEnvelope($this->getSSHKeyfile()))); 344 + // Force SVN to use `bin/ssh-connect`. 345 + $env['SVN_SSH'] = $this->getSSHWrapper(); 270 346 break; 271 347 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 272 - $command = call_user_func_array( 273 - 'csprintf', 274 - array_merge( 275 - array( 276 - "(ssh-add %P && HOME=%s git {$pattern})", 277 - new PhutilOpaqueEnvelope($this->getSSHKeyfile()), 278 - $empty, 279 - ), 280 - $args)); 281 - $pattern = "ssh-agent sh -c %s"; 282 - $args = array($command); 348 + // Force Git to use `bin/ssh-connect`. 349 + $env['GIT_SSH'] = $this->getSSHWrapper(); 283 350 break; 284 351 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 285 - $pattern = "hg --config ui.ssh=%s {$pattern}"; 286 - array_unshift( 287 - $args, 288 - csprintf( 289 - 'ssh -l %P -i %P', 290 - new PhutilOpaqueEnvelope($this->getSSHLogin()), 291 - new PhutilOpaqueEnvelope($this->getSSHKeyfile()))); 352 + // We force Mercurial through `bin/ssh-connect` too, but it uses a 353 + // command-line flag instead of an environmental variable. 292 354 break; 293 355 default: 294 356 throw new Exception("Unrecognized version control system."); 295 357 } 296 - } else if ($this->shouldUseHTTP()) { 297 - switch ($this->getVersionControlSystem()) { 298 - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 358 + } 359 + 360 + return $env; 361 + } 362 + 363 + private function formatRemoteCommand(array $args) { 364 + $pattern = $args[0]; 365 + $args = array_slice($args, 1); 366 + 367 + switch ($this->getVersionControlSystem()) { 368 + case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 369 + if ($this->shouldUseHTTP()) { 299 370 $pattern = 300 371 "svn ". 301 372 "--non-interactive ". ··· 308 379 $args, 309 380 new PhutilOpaqueEnvelope($this->getDetail('http-login')), 310 381 new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); 311 - break; 312 - default: 313 - throw new Exception( 314 - "No support for HTTP Basic Auth in this version control system."); 315 - } 316 - } else if ($this->shouldUseSVNProtocol()) { 317 - switch ($this->getVersionControlSystem()) { 318 - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 319 - $pattern = 320 - "svn ". 321 - "--non-interactive ". 322 - "--no-auth-cache ". 323 - "--username %P ". 324 - "--password %P ". 325 - $pattern; 326 - array_unshift( 327 - $args, 328 - new PhutilOpaqueEnvelope($this->getDetail('http-login')), 329 - new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); 330 - break; 331 - default: 332 - throw new Exception( 333 - "SVN protocol is SVN only."); 334 - } 335 - } else { 336 - switch ($this->getVersionControlSystem()) { 337 - case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 382 + } else if ($this->shouldUseSVNProtocol()) { 383 + $pattern = 384 + "svn ". 385 + "--non-interactive ". 386 + "--no-auth-cache ". 387 + "--username %P ". 388 + "--password %P ". 389 + $pattern; 390 + array_unshift( 391 + $args, 392 + new PhutilOpaqueEnvelope($this->getDetail('http-login')), 393 + new PhutilOpaqueEnvelope($this->getDetail('http-pass'))); 394 + } else { 338 395 $pattern = "svn --non-interactive {$pattern}"; 339 - break; 340 - case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 341 - $pattern = "HOME=%s git {$pattern}"; 342 - array_unshift($args, $empty); 343 - break; 344 - case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 396 + } 397 + break; 398 + case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 399 + $pattern = "git {$pattern}"; 400 + break; 401 + case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 402 + if ($this->shouldUseSSH()) { 403 + $pattern = "hg --config ui.ssh=%s {$pattern}"; 404 + array_unshift( 405 + $args, 406 + $this->getSSHWrapper()); 407 + } else { 345 408 $pattern = "hg {$pattern}"; 346 - break; 347 - default: 348 - throw new Exception("Unrecognized version control system."); 349 - } 409 + } 410 + break; 411 + default: 412 + throw new Exception("Unrecognized version control system."); 350 413 } 351 414 352 415 array_unshift($args, $pattern); ··· 358 421 $pattern = $args[0]; 359 422 $args = array_slice($args, 1); 360 423 361 - $empty = $this->getEmptyReadableDirectoryPath(); 362 - 363 424 switch ($this->getVersionControlSystem()) { 364 425 case PhabricatorRepositoryType::REPOSITORY_TYPE_SVN: 365 - $pattern = "(cd %s && svn --non-interactive {$pattern})"; 366 - array_unshift($args, $this->getLocalPath()); 426 + $pattern = "svn --non-interactive {$pattern}"; 367 427 break; 368 428 case PhabricatorRepositoryType::REPOSITORY_TYPE_GIT: 369 - $pattern = "(cd %s && HOME=%s git {$pattern})"; 370 - array_unshift($args, $this->getLocalPath(), $empty); 429 + $pattern = "git {$pattern}"; 371 430 break; 372 431 case PhabricatorRepositoryType::REPOSITORY_TYPE_MERCURIAL: 373 - $hgplain = (phutil_is_windows() ? "set HGPLAIN=1 &&" : "HGPLAIN=1"); 374 - $pattern = "(cd %s && {$hgplain} hg {$pattern})"; 375 - array_unshift($args, $this->getLocalPath()); 432 + $pattern = "hg {$pattern}"; 376 433 break; 377 434 default: 378 435 throw new Exception("Unrecognized version control system."); ··· 383 440 return $args; 384 441 } 385 442 386 - private function getEmptyReadableDirectoryPath() { 387 - // See T2965. Some time after Git 1.7.5.4, Git started fataling if it can 388 - // not read $HOME. For many users, $HOME points at /root (this seems to be 389 - // a default result of Apache setup). Instead, explicitly point $HOME at a 390 - // readable, empty directory so that Git looks for the config file it's 391 - // after, fails to locate it, and moves on. This is really silly, but seems 392 - // like the least damaging approach to mitigating the issue. 393 - $root = dirname(phutil_get_library_root('phabricator')); 394 - return $root.'/support/empty/'; 395 - } 396 - 397 - private function getSSHLogin() { 443 + public function getSSHLogin() { 398 444 return $this->getDetail('ssh-login'); 399 - } 400 - 401 - private function getSSHKeyfile() { 402 - if ($this->sshKeyfile === null) { 403 - $key = $this->getDetail('ssh-key'); 404 - $keyfile = $this->getDetail('ssh-keyfile'); 405 - if ($keyfile) { 406 - // Make sure we can read the file, that it exists, etc. 407 - Filesystem::readFile($keyfile); 408 - $this->sshKeyfile = $keyfile; 409 - } else if ($key) { 410 - $keyfile = new TempFile('phabricator-repository-ssh-key'); 411 - chmod($keyfile, 0600); 412 - Filesystem::writeFile($keyfile, $key); 413 - $this->sshKeyfile = $keyfile; 414 - } else { 415 - $this->sshKeyfile = ''; 416 - } 417 - } 418 - 419 - return (string)$this->sshKeyfile; 420 445 } 421 446 422 447 public function getURI() { ··· 642 667 643 668 $protocol = $this->getRemoteProtocol(); 644 669 if ($this->isSSHProtocol($protocol)) { 645 - return (bool)$this->getSSHKeyfile(); 646 - } else { 647 - return false; 670 + $key = $this->getDetail('ssh-key'); 671 + $keyfile = $this->getDetail('ssh-keyfile'); 672 + if ($key || $keyfile) { 673 + return true; 674 + } 648 675 } 676 + 677 + return false; 649 678 } 650 679 651 680