Page MenuHomePhabricator

simple CRUD 'api' for project membership
Closed, WontfixPublic

Description

I'd like a way to programatically change project membership. For example, to import members from an external source.

Summary from IRC:

  • Some way to do CRUD to project members.
  • Local script works just as well as HTTP api.
  • Binding project members to ldap groups would be "Nice-To-Have"

Event Timeline

cburroughs raised the priority of this task from to Needs Triage.
cburroughs updated the task description. (Show Details)
cburroughs added a project: Phabricator.
cburroughs added a subscriber: cburroughs.

Save this as phabricator/set-members.php:

<?php

$root = dirname(__FILE__);
require_once $root.'/scripts/__init_script__.php';

$args = new PhutilArgumentParser($argv);
$args->setTagline('set members');
$args->setSynopsis(<<<EOSYNOPSIS
cat list | **set-members.php** '#project'
  Set members of '#project' to a list of usernames.

EOSYNOPSIS
  );
$args->parseStandardArguments();
$args->parse(
  array(
    array(
      'name' => 'dry-run',
      'help' => pht('Only show what changes would be made.'),
    ),
    array(
      'name' => 'project',
      'wildcard' => true,
    ),
  ));

$viewer = PhabricatorUser::getOmnipotentUser();

$projects = $args->getArg('project');
if (count($projects) !== 1) {
  throw new Exception(pht('Specify exactly one project!'));
}

$slug = trim(head($projects), '#');
$project = id(new PhabricatorProjectQuery())
  ->setViewer($viewer)
  ->withSlugs(array($slug))
  ->needMembers(true)
  ->executeOne();

if (!$project) {
  throw new Exception(pht('No project "%s" exists!', $slug));
}

echo "Reading members from stdin...\n";
$members = file_get_contents('php://stdin');
$members = array_filter(explode("\n", $members));

$users = id(new PhabricatorPeopleQuery())
  ->setViewer($viewer)
  ->withUsernames($members)
  ->execute();
$users = mpull($users, null, 'getUsername');
foreach ($members as $member) {
  if (empty($users[$member])) {
    throw new Exception(pht('No such user "%s" exists!', $member));
  }
}
$users = mpull($users, null, 'getPHID');

$current = $project->getMemberPHIDs();
$current_users = id(new PhabricatorPeopleQuery())
  ->setViewer($viewer)
  ->withPHIDs($current)
  ->execute();

$all_users = mpull($users, null, 'getPHID') +
             mpull($current_users, null, 'getPHID');

$add = array_diff_key($users, array_fuse($current));
$rem = array_diff_key(array_fuse($current), $users);

$add_users = array_select_keys($all_users, array_keys($add));
$rem_users = array_select_keys($all_users, array_keys($rem));

if (!$add) {
  echo "No users will be added.\n\n";
} else {
  echo "These users will be added:\n";
  foreach ($add_users as $user) {
    echo "    ".$user->getUsername()."\n";
  }
  echo "\n";
}

if (!$rem) {
  echo "No users will be removed.\n\n";
} else {
  echo "These users will be removed:\n";
  foreach ($rem as $rem_phid => $ignored) {
    $name = $rem_phid;
    if (isset($rem_users[$rem_phid])) {
      $name = $rem_users[$rem_phid]->getUsername();
    }
    echo "    ".$name."\n";
  }
  echo "\n";
}

if ($args->getArg('dry-run')) {
  echo "End of dry run.\n";
  exit(0);
}

if (!$add && !$rem) {
  echo "No changes to apply.\n";
  exit(0);
}

echo "Applying changes...\n";

$type_member = PhabricatorEdgeConfig::TYPE_PROJ_MEMBER;

$editor = id(new PhabricatorEdgeEditor());
$editor->setSuppressEvents(true);

foreach ($add as $phid => $ignored) {
  $editor->addEdge($project->getPHID(), $type_member, $phid);
}
foreach ($rem as $phid => $ignored) {
  $editor->removeEdge($project->getPHID(), $type_member, $phid);
}

$editor->save();

echo "Done.\n";

Then run it as:

phabricator/ $ cat list_of_pharicator_usernames | php -f set-members.php '#project' --dry-run

You can drop --dry-run if the output looks correct.

Thanks! Will try to integrate it out as I continue our proof of concept.

In case anyone else is using this setSuppressEvents is no longer a thing and an emptyness check is needed for projects like

$current = $project->getMemberPHIDs();
$current_users = array();
if ($current) {
  $current_users = id(new PhabricatorPeopleQuery())
    ->setViewer($viewer)
    ->withPHIDs($current)
    ->execute();
}

P1266 is a similar POC script that takes a project name like Herculoids and will make the membership of all projects matching a prefix like Herculoids-Sprint the same. This might be useful for per sprint projects until T3670 to avoid the re-joining annoyance.

epriestley claimed this task.
  • See T5873 for ApplicationTransactions in Conduit (adding members).
  • See T3980 for binding to external sources.