Hello community,

here is the log from the commit of package platformsh-cli for openSUSE:Factory 
checked in at 2018-02-13 10:32:08
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/platformsh-cli (Old)
 and      /work/SRC/openSUSE:Factory/.platformsh-cli.new (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "platformsh-cli"

Tue Feb 13 10:32:08 2018 rev:35 rq:575949 version:3.29.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/platformsh-cli/platformsh-cli.changes    
2018-01-31 19:53:45.016291392 +0100
+++ /work/SRC/openSUSE:Factory/.platformsh-cli.new/platformsh-cli.changes       
2018-02-13 10:32:20.082756333 +0100
@@ -1,0 +2,18 @@
+Mon Feb 12 21:57:54 UTC 2018 - ji...@boombatower.com
+
+- Update to version 3.29.0:
+  * Release v3.29.0
+  * [db:dump] Remove --no-autocommit and simplify mysqldump args (#683)
+  * New lines in user:role to match user:add [skip changelog]
+  * Refactoring of user:add [skip changelog]
+  * Use getUsers() instead of getUser() in user:add [skip changelog]
+  * [user:add] [user:update] expand interactive help text
+  * Add `user:get` command (aliased to and deprecating `user:role`)
+  * [user:add] Improve `user:add` command to allow setting roles on all 
environments
+  * Expand redeploy warning to recommend "vset"
+  * Back to -dev
+  * [activity:get] Check for empty started_at when calculating duration
+  * [service:redis-cli] Recommend "redis info" command
+  * Merge CHANGELOG and manifest.json updates from master
+
+-------------------------------------------------------------------

Old:
----
  platformsh-cli-3.28.0.tar.xz

New:
----
  platformsh-cli-3.29.0.tar.xz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ platformsh-cli.spec ++++++
--- /var/tmp/diff_new_pack.ZePFZ7/_old  2018-02-13 10:32:21.414708338 +0100
+++ /var/tmp/diff_new_pack.ZePFZ7/_new  2018-02-13 10:32:21.422708050 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           platformsh-cli
-Version:        3.28.0
+Version:        3.29.0
 Release:        0
 Summary:        Tool for managing Platform.sh services from the command line
 # See licenses.txt for dependency licenses.

++++++ _service ++++++
--- /var/tmp/diff_new_pack.ZePFZ7/_old  2018-02-13 10:32:21.466706465 +0100
+++ /var/tmp/diff_new_pack.ZePFZ7/_new  2018-02-13 10:32:21.466706465 +0100
@@ -2,7 +2,7 @@
   <service name="tar_scm" mode="disabled">
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
-    <param name="revision">refs/tags/v3.28.0</param>
+    <param name="revision">refs/tags/v3.29.0</param>
     <param name="url">git://github.com/platformsh/platformsh-cli.git</param>
     <param name="scm">git</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.ZePFZ7/_old  2018-02-13 10:32:21.494705456 +0100
+++ /var/tmp/diff_new_pack.ZePFZ7/_new  2018-02-13 10:32:21.494705456 +0100
@@ -1,6 +1,6 @@
 <servicedata>
   <service name="tar_scm">
     <param name="url">git://github.com/platformsh/platformsh-cli.git</param>
-    <param 
name="changesrevision">c6a6802cd9b738a9a3234fe32e4476c74ba98c6c</param>
+    <param 
name="changesrevision">55eff65515f99bd641aff9905c3b664f1119fc79</param>
   </service>
 </servicedata>

++++++ platformsh-cli-3.28.0.tar.xz -> platformsh-cli-3.29.0.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/platformsh-cli-3.28.0/CHANGELOG.md 
new/platformsh-cli-3.29.0/CHANGELOG.md
--- old/platformsh-cli-3.28.0/CHANGELOG.md      2018-01-30 17:56:41.000000000 
+0100
+++ new/platformsh-cli-3.29.0/CHANGELOG.md      2018-02-12 10:10:33.000000000 
+0100
@@ -4,7 +4,17 @@
 
 More readable, curated release notes can be found at: 
https://github.com/platformsh/platformsh-cli/releases
 
-## [v3.28.0](https://github.com/platformsh/platformsh-cli/tree/v3.27.2) 
(2018-01-30)
+## [v3.29.0](https://github.com/platformsh/platformsh-cli/tree/v3.29.0) 
(2018-02-12)
+[Full 
Changelog](https://github.com/platformsh/platformsh-cli/compare/v3.28.0...v3.29.0)
+
+* [user:add] Improve `user:add` command to allow setting roles on all 
environments (aliased to `user:update`)
+* [user:get] Add `user:get` command (aliased to and deprecating `user:role`)
+* [db:dump] Remove --no-autocommit and simplify mysqldump args (#683)
+* Expand redeploy warning to recommend `vset`
+* [activity:get] Check for empty started_at when calculating duration
+* [redis] Recommend "redis info" command
+
+## [v3.28.0](https://github.com/platformsh/platformsh-cli/tree/v3.28.0) 
(2018-01-30)
 [Full 
Changelog](https://github.com/platformsh/platformsh-cli/compare/v3.27.2...v3.28.0)
 
 * Improve `activity:log` output to show more activity information.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/platformsh-cli-3.28.0/README.md 
new/platformsh-cli-3.29.0/README.md
--- old/platformsh-cli-3.28.0/README.md 2018-01-30 17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/README.md 2018-02-12 10:10:33.000000000 +0100
@@ -166,10 +166,10 @@
   tunnel:list (tunnels)                     List SSH tunnels
   tunnel:open                               Open SSH tunnels to an app's 
relationships
 user
-  user:add                                  Add a user to the project
+  user:add (user:update)                    Add a user to the project, or set 
their role(s)
   user:delete                               Delete a user from the project
+  user:get                                  View a user's role(s)
   user:list (users)                         List project users
-  user:role                                 View or change a user's role
 variable
   variable:delete                           Delete a variable from an 
environment
   variable:get (variables, vget)            View variable(s) for an environment
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/platformsh-cli-3.28.0/config.yaml 
new/platformsh-cli-3.29.0/config.yaml
--- old/platformsh-cli-3.28.0/config.yaml       2018-01-30 17:56:41.000000000 
+0100
+++ new/platformsh-cli-3.29.0/config.yaml       2018-02-12 10:10:33.000000000 
+0100
@@ -1,7 +1,7 @@
 # Metadata about the CLI application itself.
 application:
   name: 'Platform.sh CLI'
-  version: '3.28.0'
+  version: '3.29.0'
   executable: 'platform'
   package_name: 'platformsh/cli'
   installer_url: 'https://platform.sh/cli/installer'
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/platformsh-cli-3.28.0/dist/manifest.json 
new/platformsh-cli-3.29.0/dist/manifest.json
--- old/platformsh-cli-3.28.0/dist/manifest.json        2018-01-30 
17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/dist/manifest.json        2018-02-12 
10:10:33.000000000 +0100
@@ -1,10 +1,10 @@
 [
     {
         "name": "platform.phar",
-        "sha1": "4ea45e3d18eaaba7422460ef03912f68638a7ccb",
-        "sha256": 
"fcf5e8d67c198621106a242937c7d6e35eb2df5044847c447b78f3aaa31bce8a",
-        "url": 
"https://github.com/platformsh/platformsh-cli/releases/download/v3.28.0/platform.phar";,
-        "version": "3.28.0",
+        "sha1": "2f269ba903905599f2b2f6f94cde8174c551a25a",
+        "sha256": 
"7ca20c402d2514abb374fc4ec0deac04d2d1a0979e66336b3a4305806c414a38",
+        "url": 
"https://github.com/platformsh/platformsh-cli/releases/download/v3.29.0/platform.phar";,
+        "version": "3.29.0",
         "php": {
             "min": "5.5.9"
         },
@@ -102,6 +102,11 @@
                 "notes": "* Improved `activity:log` output to show more 
activity information.\n* Added `activity:get` command, hidden for now.\n* Added 
`--date-fmt` option to `activity:list` and `snapshot:list`.\n* Added detection 
for the date.timezone ini setting, and the TZ environment variable.\n* Fixed 
inverted requirement of -e/-a options in activity:log (`-a` should make `-e` 
not required).\n* Fixed user-defined aliases being prefixed with \"@\" (thanks 
to @GROwen, #677).\n* Avoid fatal error if invalid YAML config is encountered 
during updateDrushAliases().\n* Fixed SSH commands for very old OpenSSH 
versions <5.9 (using -t instead of RequestTTY).\n* Updated dependencies (mainly 
Symfony 3.4.2 -> 3.4.4).",
                 "show from": "3.27.0",
                 "hide from": "3.28.0"
+            },
+            {
+                "notes": "* [user:add] Improve `user:add` command to allow 
setting roles on all environments (aliased to `user:update`)\n* [user:get] Add 
`user:get` command (aliased to and deprecating `user:role`)\n* [db:dump] Remove 
--no-autocommit and simplify mysqldump args (#683)\n* Expand redeploy warning 
to recommend `vset`\n* [activity:get] Check for empty started_at when 
calculating duration\n* [redis] Recommend \"redis info\" command",
+                "show from": "3.28.0",
+                "hide from": "3.29.0"
             }
         ]
     }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/platformsh-cli-3.28.0/src/Command/Activity/ActivityGetCommand.php 
new/platformsh-cli-3.29.0/src/Command/Activity/ActivityGetCommand.php
--- old/platformsh-cli-3.28.0/src/Command/Activity/ActivityGetCommand.php       
2018-01-30 17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Command/Activity/ActivityGetCommand.php       
2018-02-12 10:10:33.000000000 +0100
@@ -78,10 +78,10 @@
 
         // Calculate the duration of the activity.
         if (!isset($properties['duration'])) {
-            $start = strtotime($activity->started_at);
-            $created = strtotime($activity->created_at);
             $end = strtotime($activity->isComplete() ? $activity->completed_at 
: $activity->updated_at);
-            $start = $start === $end ? $created : $start;
+            $created = strtotime($activity->created_at);
+            $start = !empty($activity->started_at) ? 
strtotime($activity->started_at) : 0;
+            $start = $start !== 0 && $start !== $end ? $start : $created;
             $properties['duration'] = $end - $start > 0 ? $end - $start : null;
         }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/platformsh-cli-3.28.0/src/Command/CommandBase.php 
new/platformsh-cli-3.29.0/src/Command/CommandBase.php
--- old/platformsh-cli-3.28.0/src/Command/CommandBase.php       2018-01-30 
17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Command/CommandBase.php       2018-02-12 
10:10:33.000000000 +0100
@@ -555,9 +555,16 @@
     protected function redeployWarning()
     {
         $this->stdErr->writeln([
-            '<comment>The remote environment must be redeployed for the change 
to take effect.</comment>',
-            "Use 'git push' with new commit(s) to trigger a redeploy."
+            '',
+            '<comment>The remote environment(s) must be redeployed for the 
change to take effect.</comment>',
+            "Use 'git push' with new commit(s) to trigger a redeploy.",
         ]);
+        if (strpos($this->getName(), 'variable:') !== 0) {
+            $this->stdErr->writeln([
+                'Alternatively, add or change an environment variable, e.g.',
+                '  <comment>' . $this->config()->get('application.executable') 
. ' vset _redeploy "$(date)"</comment>'
+            ]);
+        }
     }
 
     /**
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/platformsh-cli-3.28.0/src/Command/Db/DbDumpCommand.php 
new/platformsh-cli-3.29.0/src/Command/Db/DbDumpCommand.php
--- old/platformsh-cli-3.28.0/src/Command/Db/DbDumpCommand.php  2018-01-30 
17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Command/Db/DbDumpCommand.php  2018-02-12 
10:10:33.000000000 +0100
@@ -142,7 +142,7 @@
                 break;
 
             default:
-                $dumpCommand = 'mysqldump --no-autocommit --single-transaction 
--opt --quote-names '
+                $dumpCommand = 'mysqldump --single-transaction '
                     . $relationships->getSqlCommandArgs('mysqldump', 
$database);
                 if ($schemaOnly) {
                     $dumpCommand .= ' --no-data';
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/platformsh-cli-3.28.0/src/Command/Service/RedisCliCommand.php 
new/platformsh-cli-3.29.0/src/Command/Service/RedisCliCommand.php
--- old/platformsh-cli-3.28.0/src/Command/Service/RedisCliCommand.php   
2018-01-30 17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Command/Service/RedisCliCommand.php   
2018-02-12 10:10:33.000000000 +0100
@@ -25,7 +25,8 @@
             ->addEnvironmentOption()
             ->addAppOption();
         $this->addExample('Open the redis-cli shell');
-        $this->addExample('Ping the redis server', 'ping');
+        $this->addExample('Ping the Redis server', 'ping');
+        $this->addExample('Show Redis status information', 'info');
         $this->addExample('Scan keys', "-- --scan");
         $this->addExample('Scan keys matching a pattern', '-- "--scan 
--pattern \'*-11*\'"');
     }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/platformsh-cli-3.28.0/src/Command/User/UserAddCommand.php 
new/platformsh-cli-3.29.0/src/Command/User/UserAddCommand.php
--- old/platformsh-cli-3.28.0/src/Command/User/UserAddCommand.php       
2018-01-30 17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Command/User/UserAddCommand.php       
2018-02-12 10:10:33.000000000 +0100
@@ -2,11 +2,14 @@
 namespace Platformsh\Cli\Command\User;
 
 use Platformsh\Cli\Command\CommandBase;
+use Platformsh\Client\Model\EnvironmentAccess;
 use Platformsh\Client\Model\ProjectAccess;
-use Symfony\Component\Console\Exception\RuntimeException;
+use Symfony\Component\Console\Exception\InvalidArgumentException;
+use Symfony\Component\Console\Helper\ProgressBar;
 use Symfony\Component\Console\Input\InputArgument;
 use Symfony\Component\Console\Input\InputInterface;
 use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\NullOutput;
 use Symfony\Component\Console\Output\OutputInterface;
 use Symfony\Component\Console\Question\Question;
 
@@ -17,144 +20,272 @@
     {
         $this
             ->setName('user:add')
-            ->setDescription('Add a user to the project')
-            ->addArgument('email', InputArgument::OPTIONAL, "The new user's 
email address")
-            ->addOption('role', null, InputOption::VALUE_REQUIRED, "The new 
user's role: 'admin' or 'viewer'");
+            ->setAliases(['user:update'])
+            ->setDescription('Add a user to the project, or set their role(s)')
+            ->addArgument('email', InputArgument::OPTIONAL, "The user's email 
address")
+            ->addOption('role', 'r', InputOption::VALUE_REQUIRED | 
InputOption::VALUE_IS_ARRAY, "The user's role: 'admin' or 'viewer', or 
environment-specific role e.g. 'master:contributor' or 'stage:viewer'");
         $this->addProjectOption();
         $this->addNoWaitOption();
-        $this->addExample('Add Alice as a new administrator', 
'al...@example.com --role admin');
+        $this->addExample('Add Alice as a project admin', 'al...@example.com 
-r admin');
+        $this->addExample('Make Bob an admin on the "develop" and "stage" 
environments', 'b...@example.com -r develop:a,stage:a');
     }
 
     protected function execute(InputInterface $input, OutputInterface $output)
     {
         $this->validateInput($input);
+        $project = $this->getSelectedProject();
 
         /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
         $questionHelper = $this->getService('question_helper');
 
+        // Process the --role option.
+        $roleInput = $input->getOption('role');
+        if (count($input->getOption('role')) === 1) {
+            // Split comma-separated values.
+            $roleInput = preg_split('/[\s,]+/', reset($roleInput));
+        }
+        $specifiedProjectRole = $this->getSpecifiedProjectRole($roleInput);
+        $specifiedEnvironmentRoles = 
$this->getSpecifiedEnvironmentRoles($roleInput);
+        unset($roleInput);
+
+        // Process the [email] argument.
         $email = $input->getArgument('email');
-        if ($email && !$this->validateEmail($email)) {
-            return 1;
-        } elseif (!$email) {
-            $question = new Question('Email address: ');
-            $question->setValidator([$this, 'validateEmail']);
-            $question->setMaxAttempts(5);
-            $email = $questionHelper->ask($input, $this->stdErr, $question);
+        if (!$email) {
+            $update = stripos($input->getFirstArgument(), ':u');
+            if ($update && $input->isInteractive()) {
+                $choices = [];
+                foreach ($project->getUsers() as $access) {
+                    $account = $this->api()->getAccount($access);
+                    $choices[$account['email']] = $this->getUserLabel($access);
+                }
+                $email = $questionHelper->choose($choices, 'Enter a number to 
choose a user to update:');
+            } else {
+                $question = new Question("Enter the user's email address: ");
+                $question->setValidator(function ($answer) {
+                    return $this->validateEmail($answer);
+                });
+                $question->setMaxAttempts(5);
+                $email = $questionHelper->ask($input, $this->stdErr, 
$question);
+            }
+            $this->stdErr->writeln('');
         }
+        $this->validateEmail($email);
 
-        $project = $this->getSelectedProject();
+        // Check the user's existing role on the project.
+        $existingProjectAccess = 
$this->api()->loadProjectAccessByEmail($project, $email);
+        $existingEnvironmentRoles = [];
+        if ($existingProjectAccess) {
+            // Exit if the user is the owner already.
+            if ($existingProjectAccess->id === $project->owner) {
+                $this->stdErr->writeln(sprintf('The user %s is the owner of 
%s.', $this->getUserLabel($existingProjectAccess), 
$this->api()->getProjectLabel($project)));
+                if ($specifiedProjectRole || $specifiedEnvironmentRoles) {
+                    $this->stdErr->writeln('');
+                    $this->stdErr->writeln("<comment>The project owner's 
role(s) cannot be changed.</comment>");
 
-        if ($this->api()->loadProjectAccessByEmail($project, $email)) {
-            $this->stdErr->writeln("The user already exists: 
<comment>$email</comment>");
-            return 1;
+                    return 1;
+                }
+
+                return 0;
+            }
+
+            // Check the user's existing role(s) on the project's environments.
+            $existingEnvironmentRoles = 
$this->getEnvironmentRoles($existingProjectAccess);
         }
 
-        $projectRole = $input->getOption('role');
-        if ($projectRole && !in_array($projectRole, ProjectAccess::$roles)) {
-            $this->stdErr->writeln("Valid project-level roles are 'admin' or 
'viewer'");
-            return 1;
-        } elseif (!$projectRole) {
-            if (!$input->isInteractive()) {
-                $this->stdErr->writeln('You must specify a project role for 
the user.');
-                return 1;
+        // If the user already exists, print a summary of their roles on the
+        // project and environments.
+        if ($existingProjectAccess) {
+            $this->stdErr->writeln(sprintf('Current role(s) of <info>%s</info> 
on %s:', $this->getUserLabel($existingProjectAccess), 
$this->api()->getProjectLabel($project)));
+            $this->stdErr->writeln(sprintf('  Project role: <info>%s</info>', 
$existingProjectAccess->role));
+            foreach ($existingEnvironmentRoles as $id => $role) {
+                $this->stdErr->writeln(sprintf('    Role on <info>%s</info>: 
%s', $id, $role));
             }
-            $this->stdErr->writeln("The user's project role can be 'viewer' 
('v') or 'admin' ('a').");
-            $question = new Question('Project role <question>[V/a]</question>: 
', 'viewer');
-            $question->setValidator([$this, 'validateRole']);
-            $question->setMaxAttempts(5);
-            $projectRole = $this->standardizeRole($questionHelper->ask($input, 
$this->stdErr, $question));
         }
 
-        $environmentRoles = [];
-        $environments = [];
-        if ($projectRole !== 'admin') {
-            $environments = $this->api()->getEnvironments($project);
-            if ($input->isInteractive()) {
-                $this->stdErr->writeln(
-                    "The user's environment-level roles can be 'viewer', 
'contributor', 'admin', or 'none'."
-                );
-            }
-            foreach ($environments as $environment) {
-                $question = new Question(
-                    '<info>' . $environment->id . '</info> environment role 
<question>[v/c/a/N]</question>: ',
-                    'none'
-                );
-                $question->setValidator([$this, 'validateRole']);
-                $question->setMaxAttempts(5);
-                $environmentRoles[$environment->id] = $this->standardizeRole(
-                    $questionHelper->ask($input, $this->stdErr, $question)
-                );
+        // Resolve or merge the project role.
+        $desiredProjectRole = $specifiedProjectRole ?: ($existingProjectAccess 
? $existingProjectAccess->role : ProjectAccess::ROLE_VIEWER);
+        $provideProjectForm = !$input->getOption('role') && 
$input->isInteractive();
+        if ($provideProjectForm) {
+            if ($existingProjectAccess) {
+                $this->stdErr->writeln('');
+            }
+            $desiredProjectRole = 
$this->showProjectRoleForm($desiredProjectRole, $input);
+        }
+
+        // Resolve or merge the environment role(s).
+        $provideEnvironmentForm = $input->isInteractive()
+            && $desiredProjectRole !== ProjectAccess::ROLE_ADMIN
+            && !$specifiedEnvironmentRoles;
+        $desiredEnvironmentRoles = [];
+        if ($desiredProjectRole !== ProjectAccess::ROLE_ADMIN) {
+            foreach ($this->api()->getEnvironments($project) as $id => 
$environment) {
+                if (isset($specifiedEnvironmentRoles[$id])) {
+                    $desiredEnvironmentRoles[$id] = 
$specifiedEnvironmentRoles[$id];
+                } elseif (isset($existingEnvironmentRoles[$id])) {
+                    $desiredEnvironmentRoles[$id] = 
$existingEnvironmentRoles[$id];
+                }
+            }
+        }
+        if ($provideEnvironmentForm) {
+            if ($existingProjectAccess || $provideProjectForm) {
+                $this->stdErr->writeln('');
             }
+            $desiredEnvironmentRoles = 
$this->showEnvironmentRolesForm($desiredEnvironmentRoles, $input);
         }
 
-        $summaryFields = [
-            'Email address' => $email,
-            'Project role' => $projectRole,
-        ];
-        if (!empty($environmentRoles)) {
-            foreach ($environments as $environment) {
-                if (isset($environmentRoles[$environment->id])) {
-                    $summaryFields[$environment->title] = 
$environmentRoles[$environment->id];
+        // Build a list of the changes that are going to be made.
+        $changes = [];
+        if ($existingProjectAccess) {
+            if ($existingProjectAccess->role !== $desiredProjectRole) {
+                $changes[] = sprintf('Project role: <error>%s</error> -> 
<info>%s</info>', $existingProjectAccess->role, $desiredProjectRole);
+            }
+        } else {
+            $changes[] = sprintf('Project role: <info>%s</info>', 
$desiredProjectRole);
+        }
+        if ($desiredProjectRole !== ProjectAccess::ROLE_ADMIN) {
+            foreach ($this->api()->getEnvironments($project) as $id => 
$environment) {
+                $new = isset($desiredEnvironmentRoles[$id]) ? 
$desiredEnvironmentRoles[$id] : 'none';
+                if ($existingEnvironmentRoles) {
+                    $existing = isset($existingEnvironmentRoles[$id]) ? 
$existingEnvironmentRoles[$id] : 'none';
+                    if ($existing !== $new) {
+                        $changes[] = sprintf('Role on <info>%s</info>: 
<error>%s</error> -> <info>%s</info>', $id, $existing, $new);
+                    }
+                } elseif ($new !== 'none') {
+                    $changes[] = sprintf('Role on <info>%s</info>: 
<info>%s</info>', $id, $new);
                 }
             }
         }
 
-        $this->stdErr->writeln('Summary:');
-        foreach ($summaryFields as $field => $value) {
-            $this->stdErr->writeln("    $field: <info>$value</info>");
+        // Filter out environment roles of 'none' from the list.
+        $desiredEnvironmentRoles = array_filter($desiredEnvironmentRoles, 
function ($role) {
+            return $role !== 'none';
+        });
+
+        // Require project non-admins to be added to at least one environment.
+        if ($desiredProjectRole === ProjectAccess::ROLE_VIEWER && 
!$desiredEnvironmentRoles) {
+            $this->stdErr->writeln('');
+            $this->stdErr->writeln('<error>No environments selected.</error>');
+            $this->stdErr->writeln('A non-admin user must be added to at least 
one environment.');
+
+            if ($existingProjectAccess) {
+                $this->stdErr->writeln('');
+                $this->stdErr->writeln(sprintf(
+                    'To delete the user, run: <info>%s user:delete %s</info>',
+                    $this->config()->get('application.executable'),
+                    $this->api()->getAccount($existingProjectAccess)['email']
+                ));
+            }
+
+            return 1;
         }
 
-        $this->stdErr->writeln("<comment>Adding users can result in additional 
charges.</comment>");
+        // Prevent changing environment access for project admins.
+        if ($desiredProjectRole === ProjectAccess::ROLE_ADMIN && 
$specifiedEnvironmentRoles) {
+            $this->stdErr->writeln('');
+            $this->stdErr->writeln('<comment>A project admin has 
administrative access to all environments.</comment>');
+            $this->stdErr->writeln("To set the user's environment role(s), set 
their project role to '" . ProjectAccess::ROLE_VIEWER . "'.");
 
-        if ($input->isInteractive()) {
-            if (!$questionHelper->confirm("Are you sure you want to add this 
user?")) {
-                return 1;
+            return 1;
+        }
+
+        // Exit early if there are no changes to make.
+        if (empty($changes)) {
+            if ($provideProjectForm || $provideEnvironmentForm) {
+                $this->stdErr->writeln('');
+                $this->stdErr->writeln('There are no changes to make.');
             }
+
+            return 0;
         }
 
-        $this->stdErr->writeln("Adding the user to the project");
-        $result = $project->addUser($email, $projectRole);
-        $activities = $result->getActivities();
+        // Add a new line if there has already been output.
+        if ($existingProjectAccess || $provideProjectForm || 
$provideEnvironmentForm) {
+            $this->stdErr->writeln('');
+        }
+
+        // Print a summary of the changes that are about to be made.
+        if ($existingProjectAccess) {
+            $this->stdErr->writeln('Summary of changes:');
+        } else {
+            $this->stdErr->writeln(sprintf('Adding the user <info>%s</info> to 
%s:', $email, $this->api()->getProjectLabel($project)));
+        }
+        foreach ($changes as $change) {
+            $this->stdErr->writeln('  ' . $change);
+        }
+        $this->stdErr->writeln('');
 
-        $this->stdErr->writeln("User <info>$email</info> created");
+        // Ask for confirmation.
+        if ($existingProjectAccess) {
+            if (!$questionHelper->confirm('Are you sure you want to make these 
change(s)?')) {
 
-        $success = true;
-        if (!empty($environmentRoles)) {
+                return 1;
+            }
+        } else {
+            $this->stdErr->writeln('<comment>Adding users can result in 
additional charges.</comment>');
+            $this->stdErr->writeln('');
+            if (!$questionHelper->confirm('Are you sure you want to add this 
user?')) {
+
+                return 1;
+            }
+        }
+
+        // Make the required modifications on the project level: add the user,
+        // change their role, or do nothing.
+        if (!$existingProjectAccess) {
+            $this->stdErr->writeln("Adding the user to the project");
+            $result = $project->addUser($email, $desiredProjectRole);
+            $activities = $result->getActivities();
             /** @var ProjectAccess $projectAccess */
             $projectAccess = $result->getEntity();
             $uuid = $projectAccess->id;
-
-            $this->stdErr->writeln("Setting environment role(s)");
-            foreach ($environmentRoles as $environmentId => $role) {
-                if (!isset($environments[$environmentId])) {
-                    $this->stdErr->writeln("<error>Environment not found: 
$environmentId</error>");
-                    $success = false;
-                    continue;
-                }
-                if ($role == 'none') {
-                    continue;
-                }
-                $access = $environments[$environmentId]->getUser($uuid);
-                if ($access) {
-                    $this->stdErr->writeln("Modifying the user's role on the 
environment: <info>$environmentId</info>");
-                    $result = $access->update(['role' => $role]);
+        } elseif ($existingProjectAccess->role !== $desiredProjectRole) {
+            $this->stdErr->writeln("Setting the user's project role to: 
$desiredProjectRole");
+            $result = $existingProjectAccess->update(['role' => 
$desiredProjectRole]);
+            $activities = $result->getActivities();
+            $uuid = $existingProjectAccess->id;
+        } else {
+            $uuid = $existingProjectAccess->id;
+            $activities = [];
+        }
+
+        // Make the desired changes at the environment level.
+        if ($desiredProjectRole !== ProjectAccess::ROLE_ADMIN) {
+            foreach ($this->api()->getEnvironments($project) as $environmentId 
=> $environment) {
+                $role = isset($desiredEnvironmentRoles[$environmentId]) ? 
$desiredEnvironmentRoles[$environmentId] : 'none';
+                $access = $environment->getUser($uuid);
+                if ($role === 'none') {
+                    if ($access) {
+                        $this->stdErr->writeln("Removing the user from the 
environment <info>$environmentId</info>");
+                        $result = $access->delete();
+                    } else {
+                        continue;
+                    }
                 } else {
-                    $this->stdErr->writeln("Adding the user to the 
environment: <info>$environmentId</info>");
-                    $result = $environments[$environmentId]->addUser($uuid, 
$role);
+                    if ($access) {
+                        if ($access->role === $role) {
+                            continue;
+                        }
+                        $this->stdErr->writeln("Setting the user's role on the 
environment <info>$environmentId</info> to: $role");
+                        $result = $access->update(['role' => $role]);
+                    } else {
+                        $this->stdErr->writeln("Adding the user to the 
environment: <info>$environmentId</info>");
+                        $result = $environment->addUser($uuid, $role);
+                    }
                 }
                 $activities = array_merge($activities, 
$result->getActivities());
             }
         }
 
+        // Wait for activities to complete.
         if (!$input->getOption('no-wait')) {
             /** @var \Platformsh\Cli\Service\ActivityMonitor $activityMonitor 
*/
             $activityMonitor = $this->getService('activity_monitor');
             if (!$activityMonitor->waitMultiple($activities, $project)) {
-                $success = false;
+                return 1;
             }
         }
 
-        return $success ? 0 : 1;
+        return 0;
     }
 
     /**
@@ -162,48 +293,254 @@
      *
      * @return string
      */
-    public function validateRole($value)
+    private function validateProjectRole($value)
     {
-        if (empty($value)
-            || !in_array(strtolower($value), ['admin', 'contributor', 
'viewer', 'none', 'a', 'c', 'v', 'n'])) {
-            throw new RuntimeException("Invalid role: $value");
-        }
+        return $this->matchRole($value, ProjectAccess::$roles);
+    }
 
-        return $value;
+    /**
+     * @param string $value
+     *
+     * @return string
+     */
+    private function validateEnvironmentRole($value)
+    {
+        return $this->matchRole($value, array_merge(EnvironmentAccess::$roles, 
['none']));
     }
 
     /**
+     * Validate an email address.
+     *
      * @param string $value
      *
+     * @throws \Symfony\Component\Console\Exception\InvalidArgumentException
+     *
      * @return string
      */
-    public function validateEmail($value)
+    private function validateEmail($value)
     {
-        if (empty($value) || !filter_var($value, FILTER_VALIDATE_EMAIL)) {
-            throw new RuntimeException("Invalid email address: $value");
+        if (empty($value)) {
+            throw new InvalidArgumentException('An email address is 
required.');
+        }
+        if (!$filtered = filter_var($value, FILTER_VALIDATE_EMAIL)) {
+            throw new InvalidArgumentException('Invalid email address: ' . 
$value);
         }
 
-        return $value;
+        return $filtered;
     }
 
     /**
-     * @param string $givenRole
+     * Complete a role name based on an array of allowed roles.
+     *
+     * @param string   $input
+     * @param string[] $roles
      *
      * @return string
-     * @throws \Exception
      */
-    protected function standardizeRole($givenRole)
+    private function matchRole($input, array $roles)
     {
-        $possibleRoles = ['viewer', 'admin', 'contributor', 'none'];
-        if (in_array($givenRole, $possibleRoles)) {
-            return $givenRole;
+        foreach ($roles as $role) {
+            if (strpos($role, strtolower($input)) === 0) {
+                return $role;
+            }
         }
-        $role = strtolower($givenRole);
-        foreach ($possibleRoles as $possibleRole) {
-            if (strpos($possibleRole, $role) === 0) {
-                return $possibleRole;
+
+        throw new InvalidArgumentException('Invalid role: ' . $input);
+    }
+
+    /**
+     * Expand roles into a list with abbreviations.
+     *
+     * @param string[] $roles
+     *
+     * @return string
+     */
+    private function describeRoles(array $roles)
+    {
+        $withInitials = array_map(function ($role) {
+            return sprintf('%s (%s)', $role, substr($role, 0, 1));
+        }, $roles);
+        $last = array_pop($withInitials);
+
+        return implode(' or ', [implode(', ', $withInitials), $last]);
+    }
+
+    /**
+     * Describe the input for a roles question, e.g. [a/c/v/n].
+     *
+     * @param string[] $roles
+     *
+     * @return string
+     */
+    private function describeRoleInput(array $roles)
+    {
+        return '[' . implode('/', array_map(function ($role) {
+            return substr($role, 0, 1);
+        }, $roles)) . ']';
+    }
+
+    /**
+     * Return a label describing a user.
+     *
+     * @param \Platformsh\Client\Model\ProjectAccess $access
+     *
+     * @return string
+     */
+    private function getUserLabel(ProjectAccess $access)
+    {
+        $account = $this->api()->getAccount($access);
+
+        return sprintf('<info>%s</info> (%s)', $account['display_name'], 
$account['email']);
+    }
+
+    /**
+     * Show the form for entering the project role.
+     *
+     * @param string                                          $defaultRole
+     * @param \Symfony\Component\Console\Input\InputInterface $input
+     *
+     * @return string
+     */
+    private function showProjectRoleForm($defaultRole, InputInterface $input)
+    {
+        /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
+        $questionHelper = $this->getService('question_helper');
+
+        $this->stdErr->writeln("The user's project role can be " . 
$this->describeRoles(ProjectAccess::$roles) . '.');
+        $this->stdErr->writeln('');
+        $question = new Question(
+            sprintf('Project role (default: %s) <question>%s</question>: ', 
$defaultRole, $this->describeRoleInput(ProjectAccess::$roles)),
+            $defaultRole
+        );
+        $question->setValidator(function ($answer) {
+            return $this->validateProjectRole($answer);
+        });
+        $question->setMaxAttempts(5);
+        $question->setAutocompleterValues(ProjectAccess::$roles);
+
+        return $questionHelper->ask($input, $this->stdErr, $question);
+    }
+
+    /**
+     * Load the user's roles on the project's environments.
+     *
+     * @param \Platformsh\Client\Model\ProjectAccess $projectAccess
+     *
+     * @return array
+     */
+    private function getEnvironmentRoles(ProjectAccess $projectAccess)
+    {
+        $environmentRoles = [];
+        if ($projectAccess->role === ProjectAccess::ROLE_ADMIN) {
+            return [];
+        }
+
+        // @todo find out why $environment->getUser() has permission issues - 
it would be a lot faster than this
+
+        $progress = new ProgressBar(isset($this->stdErr) && 
$this->stdErr->isDecorated() ? $this->stdErr : new NullOutput());
+        $progress->setMessage('Loading environments...');
+        $progress->setFormat('%message% %current%/%max%');
+        $environments = 
$this->api()->getEnvironments($this->getSelectedProject());
+        $progress->start(count($environments));
+        foreach ($environments as $environment) {
+            foreach ($environment->getUsers() as $access) {
+                if ($access->user === $projectAccess->id) {
+                    $environmentRoles[$environment->id] = $access->role;
+                }
+            }
+            $progress->advance();
+        }
+        $progress->finish();
+        $progress->clear();
+
+        return $environmentRoles;
+    }
+
+    /**
+     * Show the form for entering environment roles.
+     *
+     * @param array                                           
$defaultEnvironmentRoles
+     * @param \Symfony\Component\Console\Input\InputInterface $input
+     *
+     * @return array
+     *   The environment roles (keyed by environment ID) including the user's
+     *   answers.
+     */
+    private function showEnvironmentRolesForm(array $defaultEnvironmentRoles, 
InputInterface $input)
+    {
+        /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
+        $questionHelper = $this->getService('question_helper');
+        $desiredEnvironmentRoles = [];
+        $validEnvironmentRoles = array_merge(EnvironmentAccess::$roles, 
['none']);
+        $this->stdErr->writeln("The user's environment role(s) can be " . 
$this->describeRoles($validEnvironmentRoles) . '.');
+        $initials = $this->describeRoleInput($validEnvironmentRoles);
+        $this->stdErr->writeln('');
+        foreach 
(array_keys($this->api()->getEnvironments($this->getSelectedProject())) as $id) 
{
+            $default = isset($defaultEnvironmentRoles[$id]) ? 
$defaultEnvironmentRoles[$id] : 'none';
+            $question = new Question(
+                sprintf('Role on <info>%s</info> (default: %s) 
<question>%s</question>: ', $id, $default, $initials),
+                $default
+            );
+            $question->setValidator(function ($answer) {
+                if ($answer === 'q' || $answer === 'quit') {
+                    return $answer;
+                }
+
+                return $this->validateEnvironmentRole($answer);
+            });
+            
$question->setAutocompleterValues(array_merge($validEnvironmentRoles, 
['quit']));
+            $question->setMaxAttempts(5);
+            $answer = $questionHelper->ask($input, $this->stdErr, $question);
+            if ($answer === 'q' || $answer === 'quit') {
+                break;
+            } else {
+                $desiredEnvironmentRoles[$id] = $answer;
             }
         }
-        throw new RuntimeException("Role not found: $givenRole");
+
+        return $desiredEnvironmentRoles;
+    }
+
+    /**
+     * Extract the specified project role from the list (given in --role).
+     *
+     * @param array $roles
+     *
+     * @return string|null
+     *   The project role, or null if none is specified.
+     */
+    private function getSpecifiedProjectRole(array $roles)
+    {
+        foreach ($roles as $role) {
+            if (strpos($role, ':') === false) {
+                return $this->validateProjectRole($role);
+            }
+        }
+
+        return null;
+    }
+
+    /**
+     * Extract the specified environment roles from the list (given in --role).
+     *
+     * @param string[] $roles
+     *
+     * @return array
+     *   An array of environment roles, keyed by environment ID.
+     */
+    private function getSpecifiedEnvironmentRoles(array $roles)
+    {
+        $environmentRoles = [];
+        foreach ($roles as $role) {
+            if (strpos($role, ':') !== false) {
+                list($id, $role) = explode(':', $role, 2);
+                if (!$this->api()->getEnvironment($id, 
$this->getSelectedProject())) {
+                    throw new InvalidArgumentException('Environment not found: 
' . $id);
+                }
+                $environmentRoles[$id] = $this->validateEnvironmentRole($role);
+            }
+        }
+
+        return $environmentRoles;
     }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/platformsh-cli-3.28.0/src/Command/User/UserListCommand.php 
new/platformsh-cli-3.29.0/src/Command/User/UserListCommand.php
--- old/platformsh-cli-3.28.0/src/Command/User/UserListCommand.php      
2018-01-30 17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Command/User/UserListCommand.php      
2018-02-12 10:10:33.000000000 +0100
@@ -45,6 +45,14 @@
         ksort($rows);
 
         $table->render(array_values($rows), ['Email address', 'Name', 'Project 
role', 'ID']);
+
+        if (!$table->formatIsMachineReadable()) {
+            $this->stdErr->writeln('');
+            $executable = $this->config()->get('application.executable');
+            $this->stdErr->writeln("To view a user's role(s), run: 
<info>$executable user:get [email]</info>");
+            $this->stdErr->writeln("To change a user's role(s), run: 
<info>$executable user:add [email]</info>");
+        }
+
         return 0;
     }
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/platformsh-cli-3.28.0/src/Command/User/UserRoleCommand.php 
new/platformsh-cli-3.29.0/src/Command/User/UserRoleCommand.php
--- old/platformsh-cli-3.28.0/src/Command/User/UserRoleCommand.php      
2018-01-30 17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Command/User/UserRoleCommand.php      
2018-02-12 10:10:33.000000000 +0100
@@ -12,25 +12,24 @@
 
 class UserRoleCommand extends CommandBase
 {
-
     protected function configure()
     {
         $this
-            ->setName('user:role')
-            ->setDescription("View or change a user's role")
-            ->addArgument('email', InputArgument::REQUIRED, "The user's email 
address")
-            ->addOption('role', 'r', InputOption::VALUE_REQUIRED, "A new role 
for the user")
+            ->setName('user:get')
+            ->setDescription("View a user's role(s)")
+            ->addArgument('email', InputArgument::OPTIONAL, "The user's email 
address")
             ->addOption('level', 'l', InputOption::VALUE_REQUIRED, "The role 
level ('project' or 'environment')")
-            ->addOption('pipe', null, InputOption::VALUE_NONE, 'Output the 
role only');
+            ->addOption('pipe', null, InputOption::VALUE_NONE, 'Output the 
role to stdout (after making any changes)');
         $this->addProjectOption()
              ->addEnvironmentOption()
              ->addNoWaitOption();
+
+        // Backwards compatibility.
+        $this->setHiddenAliases(['user:role']);
+        $this->addOption('role', 'r', InputOption::VALUE_REQUIRED, 
"[Deprecated: use user:update to change a user's role(s)]");
+
         $this->addExample("View Alice's role on the project", 
'al...@example.com');
         $this->addExample("View Alice's role on the environment", 
'al...@example.com --level environment');
-        $this->addExample(
-            "Give Alice the 'contributor' role on the environment 'test'",
-            'al...@example.com --level environment --environment test --role 
contributor'
-        );
     }
 
     protected function execute(InputInterface $input, OutputInterface $output)
@@ -46,15 +45,37 @@
         $this->validateInput($input, $level !== 'environment');
         $project = $this->getSelectedProject();
 
+        $this->warnAboutDeprecatedOptions(['role']);
+
+        /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
+        $questionHelper = $this->getService('question_helper');
+
+        // Load the user.
+        $email = $input->getArgument('email');
+        if ($email === null && $input->isInteractive()) {
+            $choices = [];
+            foreach ($project->getUsers() as $access) {
+                $account = $this->api()->getAccount($access);
+                $choices[$account['email']] = sprintf('%s (%s)', 
$account['display_name'], $account['email']);
+            }
+            $email = $questionHelper->choose($choices, 'Enter a number to 
choose a user:');
+            $this->stdErr->writeln('');
+        }
+        $projectAccess = $this->api()->loadProjectAccessByEmail($project, 
$email);
+        if (!$projectAccess) {
+            $this->stdErr->writeln("User not found: <error>$email</error>");
+
+            return 1;
+        }
+
         if ($level === null && $role && $this->hasSelectedEnvironment() && 
$input->isInteractive()) {
             $environment = $this->getSelectedEnvironment();
-            /** @var \Platformsh\Cli\Service\QuestionHelper $questionHelper */
-            $questionHelper = $this->getService('question_helper');
-            $question = new ChoiceQuestion('For which access level do you want 
to set the role?', [
+            $question = new ChoiceQuestion('What role level do you want to set 
to "' . $role . '"?', [
                 'project' => 'The project',
                 'environment' => sprintf('The environment (%s)', 
$environment->id),
             ]);
             $level = $questionHelper->ask($input, $output, $question);
+            $this->stdErr->writeln('');
         } elseif ($level === null && $role) {
             $level = 'project';
         }
@@ -69,100 +90,35 @@
             return 1;
         }
 
-        // Load the user.
-        $email = $input->getArgument('email');
-        $projectAccess = $this->api()->loadProjectAccessByEmail($project, 
$email);
-        if (!$projectAccess) {
-            $this->stdErr->writeln("User not found: <error>$email</error>");
-
-            return 1;
-        }
-
-        // Get the current role.
-        if ($level !== 'environment') {
-            $currentRole = $projectAccess->role;
-            $environmentAccess = false;
-        } else {
-            $environmentAccess = 
$this->getSelectedEnvironment()->getUser($projectAccess->id);
-            $currentRole = $environmentAccess === false ? 'none' : 
$environmentAccess->role;
-        }
-
-        if ($role === $currentRole) {
-            $this->stdErr->writeln("There is nothing to change");
-        } elseif ($role && $project->owner === $projectAccess->id) {
-            $this->stdErr->writeln(sprintf(
-                'The user <error>%s</error> is the owner of the project %s.',
-                $email,
-                $this->api()->getProjectLabel($project, 'error')
-            ));
-            $this->stdErr->writeln("You cannot change the role of the 
project's owner.");
-            return 1;
-        } elseif ($role && $level === 'environment' && $projectAccess->role 
=== ProjectAccess::ROLE_ADMIN) {
-            $this->stdErr->writeln(sprintf(
-                'The user <error>%s</error> is an admin on the project %s.',
-                $email,
-                $this->api()->getProjectLabel($project, 'error')
-            ));
-            $this->stdErr->writeln('You cannot change the environment-level 
role of a project admin.');
-            return 1;
-        } elseif ($role && $level !== 'environment') {
-            $result = $projectAccess->update(['role' => $role]);
-            $this->stdErr->writeln("User <info>$email</info> updated");
-        } elseif ($role && $level === 'environment') {
-            $environment = $this->getSelectedEnvironment();
-            if ($role === 'none') {
-                if ($environmentAccess instanceof EnvironmentAccess) {
-                    $result = $environmentAccess->delete();
-                }
-            } elseif ($environmentAccess instanceof EnvironmentAccess) {
-                $result = $environmentAccess->update(['role' => $role]);
-            } else {
-                $result = $environment->addUser($projectAccess->id, $role);
+        $args = [
+            'email' => $email,
+            '--role' => [],
+            '--project' => $project->id,
+        ];
+        if ($role) {
+            if ($level === 'project') {
+                $args['--role'][] = $role;
+            } elseif ($level === 'environment') {
+                $args['--role'][] = $this->getSelectedEnvironment()->id . ':' 
. $role;
             }
-            $this->stdErr->writeln("User <info>$email</info> updated");
+        } else {
+            $args['--yes'] = true;
         }
-
-        if (isset($result) && !$input->getOption('no-wait')) {
-            /** @var \Platformsh\Cli\Service\ActivityMonitor $activityMonitor 
*/
-            $activityMonitor = $this->getService('activity_monitor');
-            $activityMonitor->waitMultiple($result->getActivities(), $project);
+        $result = $this->runOtherCommand($role ? 'user:update' : 'user:add', 
$args, $output);
+        if ($result !== 0) {
+            return $result;
         }
 
         if ($input->getOption('pipe')) {
+            $uuid = $projectAccess->id;
             if ($level !== 'environment') {
-                $output->writeln($projectAccess->role);
+                $projectAccess = 
$this->api()->loadProjectAccessByEmail($project, $email);
+                $currentRole = $projectAccess ? $projectAccess->role : 'none';
             } else {
-                $access = 
$this->getSelectedEnvironment()->getUser($projectAccess->id);
-                $output->writeln($access ? $access->role : 'none');
-            }
-
-            return 0;
-        }
-
-        if ($level !== 'environment') {
-            $output->writeln("Project role: 
<info>{$projectAccess->role}</info>");
-        }
-
-        $environments = [];
-        if ($level === 'environment') {
-            $environments = [$this->getSelectedEnvironment()];
-        } elseif ($level === null && $projectAccess->role !== 
ProjectAccess::ROLE_ADMIN) {
-            $environments = $this->api()->getEnvironments($project);
-            $this->api()->sortResources($environments, 'id');
-            if ($this->hasSelectedEnvironment()) {
-                $environment = $this->getSelectedEnvironment();
-                unset($environments[$environment->id]);
-                array_splice($environments, 0, 0, [$environment->id => 
$environment]);
+                $environmentAccess = 
$this->getSelectedEnvironment()->getUser($uuid);
+                $currentRole = $environmentAccess ? $environmentAccess->role : 
'none';
             }
-        }
-
-        foreach ($environments as $environment) {
-            $access = $environment->getUser($projectAccess->id);
-            $output->writeln(sprintf(
-                'Role for environment %s: <info>%s</info>',
-                $environment->id,
-                $access ? $access->role : 'none'
-            ));
+            $output->writeln($currentRole);
         }
 
         return 0;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/platformsh-cli-3.28.0/src/Service/Api.php 
new/platformsh-cli-3.29.0/src/Service/Api.php
--- old/platformsh-cli-3.28.0/src/Service/Api.php       2018-01-30 
17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Service/Api.php       2018-02-12 
10:10:33.000000000 +0100
@@ -52,6 +52,9 @@
     /** @var bool */
     protected static $environmentsCacheRefreshed = false;
 
+    /** @var \Platformsh\Client\Model\Account[] */
+    protected static $accountsCache = [];
+
     /** @var array */
     protected static $notFound = [];
 
@@ -426,10 +429,15 @@
      */
     public function getAccount(ProjectAccess $user, $reset = false)
     {
+        if (isset(self::$accountsCache[$user->id]) && !$reset) {
+            return self::$accountsCache[$user->id];
+        }
+
         $cacheKey = 'account:' . $user->id;
         if ($reset || !($details = $this->cache->fetch($cacheKey))) {
             $details = $user->getAccount()->getProperties();
             $this->cache->save($cacheKey, $details, 
$this->config->get('api.users_ttl'));
+            self::$accountsCache[$user->id] = $details;
         }
 
         return $details;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/platformsh-cli-3.28.0/src/Service/PropertyFormatter.php 
new/platformsh-cli-3.29.0/src/Service/PropertyFormatter.php
--- old/platformsh-cli-3.28.0/src/Service/PropertyFormatter.php 2018-01-30 
17:56:41.000000000 +0100
+++ new/platformsh-cli-3.29.0/src/Service/PropertyFormatter.php 2018-02-12 
10:10:33.000000000 +0100
@@ -42,7 +42,8 @@
             case 'started_at':
             case 'completed_at':
             case 'ssl.expires_on':
-                return $this->formatDate($value);
+                $value = $this->formatDate($value);
+                break;
 
             case 'ssl':
                 if ($property === 'ssl' && is_array($value) && 
isset($value['expires_on'])) {
@@ -76,7 +77,7 @@
     /**
      * @param string $value
      *
-     * @return string
+     * @return string|null
      */
     protected function formatDate($value)
     {
@@ -94,7 +95,7 @@
 
         $timestamp = is_numeric($value) ? $value : strtotime($value);
 
-        return date($format, $timestamp);
+        return $timestamp === false ? null : date($format, $timestamp);
     }
 
     /**

++++++ platformsh-cli-vendor.tar.xz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/autoload.php new/vendor/autoload.php
--- old/vendor/autoload.php     2018-01-31 04:51:30.626379513 +0100
+++ new/vendor/autoload.php     2018-02-12 22:57:57.119722174 +0100
@@ -4,4 +4,4 @@
 
 require_once __DIR__ . '/composer/autoload_real.php';
 
-return ComposerAutoloaderInit356ede5f5e6cb0e287cfa8ee0def1ebe::getLoader();
+return ComposerAutoloaderInit1000482ab31b6ea30c0fe42bed510766::getLoader();
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/composer/autoload_real.php 
new/vendor/composer/autoload_real.php
--- old/vendor/composer/autoload_real.php       2018-01-31 04:51:30.626379513 
+0100
+++ new/vendor/composer/autoload_real.php       2018-02-12 22:57:57.119722174 
+0100
@@ -2,7 +2,7 @@
 
 // autoload_real.php @generated by Composer
 
-class ComposerAutoloaderInit356ede5f5e6cb0e287cfa8ee0def1ebe
+class ComposerAutoloaderInit1000482ab31b6ea30c0fe42bed510766
 {
     private static $loader;
 
@@ -19,15 +19,15 @@
             return self::$loader;
         }
 
-        
spl_autoload_register(array('ComposerAutoloaderInit356ede5f5e6cb0e287cfa8ee0def1ebe',
 'loadClassLoader'), true, true);
+        
spl_autoload_register(array('ComposerAutoloaderInit1000482ab31b6ea30c0fe42bed510766',
 'loadClassLoader'), true, true);
         self::$loader = $loader = new \Composer\Autoload\ClassLoader();
-        
spl_autoload_unregister(array('ComposerAutoloaderInit356ede5f5e6cb0e287cfa8ee0def1ebe',
 'loadClassLoader'));
+        
spl_autoload_unregister(array('ComposerAutoloaderInit1000482ab31b6ea30c0fe42bed510766',
 'loadClassLoader'));
 
         $useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') 
&& (!function_exists('zend_loader_file_encoded') || 
!zend_loader_file_encoded());
         if ($useStaticLoader) {
             require_once __DIR__ . '/autoload_static.php';
 
-            
call_user_func(\Composer\Autoload\ComposerStaticInit356ede5f5e6cb0e287cfa8ee0def1ebe::getInitializer($loader));
+            
call_user_func(\Composer\Autoload\ComposerStaticInit1000482ab31b6ea30c0fe42bed510766::getInitializer($loader));
         } else {
             $map = require __DIR__ . '/autoload_namespaces.php';
             foreach ($map as $namespace => $path) {
@@ -48,19 +48,19 @@
         $loader->register(true);
 
         if ($useStaticLoader) {
-            $includeFiles = 
Composer\Autoload\ComposerStaticInit356ede5f5e6cb0e287cfa8ee0def1ebe::$files;
+            $includeFiles = 
Composer\Autoload\ComposerStaticInit1000482ab31b6ea30c0fe42bed510766::$files;
         } else {
             $includeFiles = require __DIR__ . '/autoload_files.php';
         }
         foreach ($includeFiles as $fileIdentifier => $file) {
-            composerRequire356ede5f5e6cb0e287cfa8ee0def1ebe($fileIdentifier, 
$file);
+            composerRequire1000482ab31b6ea30c0fe42bed510766($fileIdentifier, 
$file);
         }
 
         return $loader;
     }
 }
 
-function composerRequire356ede5f5e6cb0e287cfa8ee0def1ebe($fileIdentifier, 
$file)
+function composerRequire1000482ab31b6ea30c0fe42bed510766($fileIdentifier, 
$file)
 {
     if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
         require $file;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/vendor/composer/autoload_static.php 
new/vendor/composer/autoload_static.php
--- old/vendor/composer/autoload_static.php     2018-01-31 04:51:30.626379513 
+0100
+++ new/vendor/composer/autoload_static.php     2018-02-12 22:57:57.119722174 
+0100
@@ -4,7 +4,7 @@
 
 namespace Composer\Autoload;
 
-class ComposerStaticInit356ede5f5e6cb0e287cfa8ee0def1ebe
+class ComposerStaticInit1000482ab31b6ea30c0fe42bed510766
 {
     public static $files = array (
         '0e6d7bf4a5811bfa5cf40c5ccd6fae6a' => __DIR__ . '/..' . 
'/symfony/polyfill-mbstring/bootstrap.php',
@@ -189,9 +189,9 @@
     public static function getInitializer(ClassLoader $loader)
     {
         return \Closure::bind(function () use ($loader) {
-            $loader->prefixLengthsPsr4 = 
ComposerStaticInit356ede5f5e6cb0e287cfa8ee0def1ebe::$prefixLengthsPsr4;
-            $loader->prefixDirsPsr4 = 
ComposerStaticInit356ede5f5e6cb0e287cfa8ee0def1ebe::$prefixDirsPsr4;
-            $loader->classMap = 
ComposerStaticInit356ede5f5e6cb0e287cfa8ee0def1ebe::$classMap;
+            $loader->prefixLengthsPsr4 = 
ComposerStaticInit1000482ab31b6ea30c0fe42bed510766::$prefixLengthsPsr4;
+            $loader->prefixDirsPsr4 = 
ComposerStaticInit1000482ab31b6ea30c0fe42bed510766::$prefixDirsPsr4;
+            $loader->classMap = 
ComposerStaticInit1000482ab31b6ea30c0fe42bed510766::$classMap;
 
         }, null, ClassLoader::class);
     }


Reply via email to