Files
phabricator/scripts/gitolite/rebuild_gitolite.php
Sergey Sharybin f72b9824f3 Do not overwrite files if they did not change
This causes an extra file read, but the benefit is that it is easier
to compare data on the file system, quickly see what did actually change
and what did not.
2020-11-04 11:39:40 +01:00

493 lines
14 KiB
PHP
Executable File

#!/usr/local/bin/php
<?php
$root = dirname(dirname(dirname(__FILE__)));
require_once $root.'/scripts/__init_script__.php';
////////////////////////////////////////////////////////////////////////////////
// Utilities.
//
// TODO(sergey): Move somewhere else. Or, evenmore ideally, use Phabricator's
// utilities instead.
function escape_name($name) {
return preg_replace('/[^A-Za-z0-9\-]/', '_', $name);
}
function startswith($string, $prefix) {
return substr($string, 0, strlen($prefix)) == $prefix;
}
function endswith($string, $suffix) {
$suffix_length = strlen($suffix);
return substr($string, strlen($string) - $suffix_length,
$suffix_length) == $suffix;
}
function file_put_contents_if_different($file_name, $content) {
if (file_exists($file_name)) {
$current_content = file_get_contents($file_name);
if ($current_content == $content) {
return;
}
}
file_put_contents($file_name, $content);
}
////////////////////////////////////////////////////////////////////////////////
// Phabricator access list traversal.
class Configuration {
// Phabricator user which is used as a viewer.
public $viewer;
// Directory where public keys are stored.
// Full path.
protected $keys_directory;
// Gitolite configuration file (gitolite.conf).
// Full path.
protected $config_file;
// Indexed by key content, contains configuration user name.
protected $system_keys;
// Indexed by config user name.
protected $used_keys;
// Indexed by committers variable name, contains list of users which are
// configured by Phabricator to be able to commit to the repository.
protected $committers;
public function __construct($gitolite_root) {
$this->viewer = PhabricatorUser::getOmnipotentUser();
$this->keys_directory = "$gitolite_root/keydir";
$this->config_file = "$gitolite_root/conf/gitolite.conf";
$this->collectSystemPublicKeys();
if (!file_exists($this->config_file)) {
die("Not found: $this->config_file\n");
}
}
// Store given key of given user.
//
// Includes both storing public key in the file, and storing mapping between
// user and the key.
public function storeUserPublicKey($user, $key) {
$full_key_content = $this->getPublicKeyContent($key);
if (array_key_exists($full_key_content, $this->system_keys)) {
return $this->system_keys[$full_key_content];
}
$config_user_name = $this->getConfigUserName($user, $key);
if (!array_key_exists($config_user_name, $this->used_keys)) {
$this->used_keys[$config_user_name] = true;
file_put_contents_if_different("$this->keys_directory/$config_user_name.pub",
$full_key_content);
}
return $config_user_name;
}
public function setRepositoryUsers($repository, $config_user_names) {
$uri = $repository->getRemoteURI();
$repository_name = basename($uri, '.git');
$variable_name = '@committers_' . escape_name(strtolower($repository_name));
$this->committers[$variable_name] = $config_user_names;
}
public function writeNewConfiguration() {
$current_config = file_get_contents($this->config_file);
$current_config_lines = explode("\n", $current_config);
$new_config = "";
foreach ($current_config_lines as $line) {
if (startswith($line, '@committers_')) {
$parts = explode('=', $line);
$variable_name = trim($parts[0]);
if (array_key_exists($variable_name, $this->committers)) {
$system_committers = $this->getNonPhabtricatorUsers($parts[1]);
$committers = implode(' ', array_merge(
$system_committers, $this->committers[$variable_name]));
$line = "$variable_name = $committers";
}
}
$new_config .= $line . "\n";
}
file_put_contents_if_different($this->config_file, trim($new_config) . "\n");
}
protected function getNonPhabtricatorUsers($configuration_value) {
$system_users = array();
$users = explode(' ', $configuration_value);
foreach ($users as $user) {
$user = trim($user);
if (empty($user)) {
continue;
}
if (startswith($user, 'PHAB')) {
continue;
}
$system_users[] = $user;
}
return $system_users;
}
public function finalize() {
$this->removeUnusedPublicKeys();
}
// Get content of a public key to be stored in file.
protected function getPublicKeyContent($key) {
return $key->getKeyType().' '.
$key->getKeyBody().' '.
$key->getKeyComment()."\n";
}
// Get user+key name used by the Gitolite configuration.
protected function getConfigUserName($user, $key) {
$escaped_key_name = escape_name($key->getName());
return 'PHAB_'.$user->getUserName().
'_'.$escaped_key_name.
'_'.$key->getID();
}
// Get keys which are not managed by this Phabricator/Git integration script.
//
// Returns map from key content to the key file name. This is used to avoid
// public key duplication in the case system key is used by phabricator user.
protected function collectSystemPublicKeys() {
$files = scandir($this->keys_directory);
foreach ($files as $file) {
if (startswith($file, "PHAB")) {
continue;
}
if (!endswith($file, '.pub')) {
continue;
}
$key = file_get_contents("$this->keys_directory/$file");
$file_we = basename($file, '.pub');
$this->system_keys[$key] = $file_we;
$this->used_keys[$file_we] = true;
}
}
protected function removeUnusedPublicKeys() {
$files = scandir($this->keys_directory);
foreach ($files as $file) {
if (!startswith($file, "PHAB")) {
continue;
}
$config_user_name = basename($file, '.pub');
if (!array_key_exists($config_user_name, $this->used_keys)) {
unlink("$this->keys_directory/$file");
}
}
}
};
function getProjectMembersPHIDs($viewer, $project_phid) {
$project = id(new PhabricatorProjectQuery())
->setViewer($viewer)
->needMembers(true)
->withPHIDs(array($project_phid))
->executeOne();
return $project->getMemberPHIDs();
}
// Get user's heys and put them to the configuration
function handleSingleUserPHID($config, $userPHID) {
$user = id(new PhabricatorPeopleQuery())
->setViewer($config->viewer)
->withPHIDs(array($userPHID))
->executeOne();
if (!$user) {
return array();
}
if ($user->getIsDisabled()) {
return array();
}
$keys = id(new PhabricatorAuthSSHKey())->loadAllWhere(
'objectPHID = %s',
$user->getPHID());
$config_user_names = array();
foreach ($keys as $key) {
$config_user_name = $config->storeUserPublicKey($user, $key);
$config_user_names[] = $config_user_name;
}
return $config_user_names;
}
function handleUsersPolicyRule($config, $rule) {
$config_user_names = array();
foreach ($rule['value'] as $userPHID) {
$config_user_names = array_merge($config_user_names,
handleSingleUserPHID($config, $userPHID));
}
return $config_user_names;
}
function handleProjectsPolicyRule($config, $rule) {
$config_user_names = array();
foreach ($rule['value'] as $projectPHID) {
$memberPHIDs = getProjectMembersPHIDs($config->viewer, $projectPHID);
foreach ($memberPHIDs as $userPHID) {
$config_user_names = array_merge($config_user_names,
handleSingleUserPHID($config, $userPHID));
}
}
return $config_user_names;
}
function handleProjectsAllPolicyRule($config, $rule) {
$is_first_project = true;
$allowed_members_phids = array();
foreach ($rule['value'] as $project_phid) {
$memberPHIDs = getProjectMembersPHIDs($config->viewer, $project_phid);
if ($is_first_project) {
$allowed_members_phids = $memberPHIDs;
$is_first_project = false;
} else {
$allowed_members_phids = array_intersect(
$allowed_members_phids, $memberPHIDs);
}
}
$config_user_names = array();
foreach ($allowed_members_phids as $userPHID) {
$config_user_names = array_merge($config_user_names,
handleSingleUserPHID($config, $userPHID));
}
return $config_user_names;
}
function handleAdministratorsPolicyRule($config, $rule) {
$administrators = id(new PhabricatorPeopleQuery())
->setViewer($config->viewer)
->withIsAdmin(true)
->execute();
$config_user_names = array();
foreach ($administrators as $administrator) {
$config_user_names = array_merge($config_user_names,
handleSingleUserPHID($config, $administrator->getPHID()));
}
return $config_user_names;
}
function handleLegalpadSingleDocument($config, $document) {
if ($document->getSignatureType() !=
LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL) {
return array();
}
$config_user_names = array();
foreach ($document->getSignatures() as $signature) {
if ($signature->getSignatureType() !=
LegalpadDocument::SIGNATURE_TYPE_INDIVIDUAL) {
continue;
}
$config_user_names = array_merge($config_user_names,
handleSingleUserPHID($config, $signature->getSignerPHID()));
}
return $config_user_names;
}
function handleLegalpadSignaturePolicyRule($config, $rule) {
$documents = id(new LegalpadDocumentQuery())
->setViewer($config->viewer)
->withPHIDs($rule['value'])
->needSignatures(true)
->execute();
$config_user_names = array();
foreach ($documents as $document) {
$config_user_names = array_merge($config_user_names,
handleLegalpadSingleDocument($config, $document));
}
return $config_user_names;
}
function handleCustomPolicy($config, $policy) {
$config_user_names = array();
$rules = $policy->getRules();
foreach ($rules as $rule) {
// Everyone is denied by default anyway
if ($rule['action'] != 'allow') {
continue;
}
$policy_config_user_names = array();
$rule_type = $rule['rule'];
if ($rule_type == 'PhabricatorPolicyRuleUsers') {
$policy_config_user_names =
handleUsersPolicyRule($config, $rule);
} else if ($rule_type == 'PhabricatorProjectsPolicyRule') {
$policy_config_user_names =
handleProjectsPolicyRule($config, $rule);
} else if ($rule_type == 'PhabricatorProjectsAllPolicyRule') {
$policy_config_user_names =
handleProjectsAllPolicyRule($config, $rule);
} else if ($rule_type == 'PhabricatorAdministratorsPolicyRule') {
$policy_config_user_names =
handleAdministratorsPolicyRule($config, $rule);
} else if ($rule_type == 'PhabricatorLegalpadSignaturePolicyRule') {
$policy_config_user_names =
handleLegalpadSignaturePolicyRule($config, $rule);
}
$config_user_names = array_merge(
$config_user_names, $policy_config_user_names);
}
return $config_user_names;
}
// Parse repository and put it's members to the config file
function handleSingleRepository($config, $repository) {
$policies = PhabricatorPolicyQuery::loadPolicies(
$config->viewer,
$repository);
$pushable = $policies[DiffusionPushCapability::CAPABILITY];
$type = $pushable->getType();
$config_user_names = array();
if ($type == PhabricatorPolicyType::TYPE_PROJECT) {
$project = id(new PhabricatorProjectQuery())
->setViewer($config->viewer)
->needMembers(true)
->withPHIDs(array($pushable->getPHID()))
->executeOne();
$memberPHIDs = $project->getMemberPHIDs();
foreach ($memberPHIDs as $memberPHID) {
$config_user_names = array_merge($config_user_names,
handleSingleUserPHID($config, $memberPHID));
}
} else if ($type == PhabricatorPolicyType::TYPE_USER) {
$config_user_names = handleSingleUserPHID($config, $pushable->getPHID());
} else if ($type == PhabricatorPolicyType::TYPE_CUSTOM) {
$config_user_names = handleCustomPolicy($config, $pushable);
} else {
/* pass */
}
$config->setRepositoryUsers($repository, $config_user_names);
}
function rebuildConfiguration($gitolite_root) {
$config = new Configuration($gitolite_root);
// Fill in new configuration and keys
$used_keys = array();
$repositories = id(new PhabricatorRepositoryQuery())
->setViewer($config->viewer)
->execute();
foreach ($repositories as $repository_id => $repository) {
$type = $repository->getVersionControlSystem();
if ($type == PhabricatorRepositoryType::REPOSITORY_TYPE_GIT) {
handleSingleRepository($config, $repository);
}
}
$config->writeNewConfiguration();
$config->finalize();
return true;
}
////////////////////////////////////////////////////////////////////////////////
// Repository manipulation functionality.
function getGitCommand($repository) {
$git_dir = realpath("$repository/.git");
$git = "git --git-dir='$git_dir'";
$git .= ' --work-tree='.realpath($repository);
return $git;
}
function runGitCommand($repository, $arguments,
&$output=null, &$return_var=null) {
$git = getGitCommand($repository);
$git .= " $arguments";
exec($git, $output, $return_var);
return $return_var == 0;
}
function runGitSshCommand($repository, $key, $arguments,
&$output=null, &$return_var=null) {
$abs_key = realpath($key);
$git = "GIT_SSH_COMMAND=\"ssh -i $key -o IdentitiesOnly=yes\" ";
$git .= getGitCommand($repository);
$git .= " $arguments";
exec($git, $output, $return_var);
return $return_var == 0;
}
function repositoryPull($repository, $key) {
return runGitSshCommand($repository, $key, 'pull --rebase');
}
function repositoryCommitAll($repository, $author, $message) {
if (!runGitCommand(
$repository, 'ls-files --other --exclude-standard', $untracked_files)) {
return false;
}
if (count($untracked_files)) {
$flat_files = join(' ', $untracked_files);
if (!runGitCommand($repository, "add $flat_files")) {
return false;
}
}
runGitCommand($repository, "update-index -q --refresh", $output);
runGitCommand($repository, "diff-index --name-only HEAD --", $output);
if (count($output)) {
return runGitCommand(
$repository, "commit --author='$author' -a -m '$message'", $output);
}
return true;
}
if (count($argv) != 3) {
print("Usage: {$argv[0]} /path/to/gitolite-admin /path/to/id_rsa.pub\n");
exit(1);
}
$gitolite_root = $argv[1];
$key = $argv[2];
if (!repositoryPull($gitolite_root, $key)) {
print("Failed to pull changes from server.\n");
exit(1);
}
if (!rebuildConfiguration($gitolite_root)) {
exit(1);
}
if (!repositoryCommitAll(
$gitolite_root, 'Rebuild Gitadmin <null@git.blender.org>',
'Update to correspond changes in Phabricator')) {
print("Failed to commit changes.\n");
exit(1);
}
runGitSshCommand($gitolite_root, $key, 'push origin master');
?>