Initial implementation of gitolite integration script

Is based on old gitosis/gitadmin script, but it is heavily refactored
in the process of migration.
This commit is contained in:
2020-11-02 16:09:49 +01:00
parent e0bd65be95
commit a5efb1e8cd

View File

@@ -0,0 +1,483 @@
#!/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;
}
////////////////////////////////////////////////////////////////////////////////
// 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->system_keys = $this->getSystemPublicKeys();
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("$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($this->config_file, $new_config);
}
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 getSystemPublicKeys() {
$files = scandir($this->keys_directory);
$system_keys = array();
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;
}
return $system_keys;
}
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($system_keys, $used_keys);
} else if ($rule_type == 'PhabricatorProjectsPolicyRule') {
$policy_config_user_names =
handleProjectsPolicyRule($system_keys, $used_keys);
} else if ($rule_type == 'PhabricatorProjectsAllPolicyRule') {
$policy_config_user_names =
handleProjectsAllPolicyRule($system_keys, $used_keys);
} else if ($rule_type == 'PhabricatorAdministratorsPolicyRule') {
$policy_config_user_names =
handleAdministratorsPolicyRule($system_keys, $used_keys);
} else if ($rule_type == 'PhabricatorLegalpadSignaturePolicyRule') {
$policy_config_user_names =
handleLegalpadSignaturePolicyRule($system_keys, $used_keys);
}
$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');
}
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');
?>