--- /dev/null
+<?php
+namespace Robo\Task\Remote;
+
+use Robo\Contract\CommandInterface;
+use Robo\Task\BaseTask;
+use Robo\Exception\TaskException;
+
+/**
+ * Executes rsync in a flexible manner.
+ *
+ * ``` php
+ * $this->taskRsync()
+ * ->fromPath('src/')
+ * ->toHost('localhost')
+ * ->toUser('dev')
+ * ->toPath('/var/www/html/app/')
+ * ->remoteShell('ssh -i public_key')
+ * ->recursive()
+ * ->excludeVcs()
+ * ->checksum()
+ * ->wholeFile()
+ * ->verbose()
+ * ->progress()
+ * ->humanReadable()
+ * ->stats()
+ * ->run();
+ * ```
+ *
+ * You could also clone the task and do a dry-run first:
+ *
+ * ``` php
+ * $rsync = $this->taskRsync()
+ * ->fromPath('src/')
+ * ->toPath('example.com:/var/www/html/app/')
+ * ->archive()
+ * ->excludeVcs()
+ * ->progress()
+ * ->stats();
+ *
+ * $dryRun = clone $rsync;
+ * $dryRun->dryRun()->run();
+ * if ('y' === $this->ask('Do you want to run (y/n)')) {
+ * $rsync->run();
+ * }
+ * ```
+ */
+class Rsync extends BaseTask implements CommandInterface
+{
+ use \Robo\Common\ExecOneCommand;
+
+ /**
+ * @var string
+ */
+ protected $command;
+
+ /**
+ * @var string
+ */
+ protected $fromUser;
+
+ /**
+ * @var string
+ */
+ protected $fromHost;
+
+ /**
+ * @var string
+ */
+ protected $fromPath;
+
+ /**
+ * @var string
+ */
+ protected $toUser;
+
+ /**
+ * @var string
+ */
+ protected $toHost;
+
+ /**
+ * @var string
+ */
+ protected $toPath;
+
+ /**
+ * @return static
+ */
+ public static function init()
+ {
+ return new static();
+ }
+
+ public function __construct()
+ {
+ $this->command = 'rsync';
+ }
+
+ /**
+ * This can either be a full rsync path spec (user@host:path) or just a path.
+ * In case of the former do not specify host and user.
+ *
+ * @param string|array $path
+ *
+ * @return $this
+ */
+ public function fromPath($path)
+ {
+ $this->fromPath = $path;
+
+ return $this;
+ }
+
+ /**
+ * This can either be a full rsync path spec (user@host:path) or just a path.
+ * In case of the former do not specify host and user.
+ *
+ * @param string $path
+ *
+ * @return $this
+ */
+ public function toPath($path)
+ {
+ $this->toPath = $path;
+
+ return $this;
+ }
+
+ /**
+ * @param string $fromUser
+ *
+ * @return $this
+ */
+ public function fromUser($fromUser)
+ {
+ $this->fromUser = $fromUser;
+ return $this;
+ }
+
+ /**
+ * @param string $fromHost
+ *
+ * @return $this
+ */
+ public function fromHost($fromHost)
+ {
+ $this->fromHost = $fromHost;
+ return $this;
+ }
+
+ /**
+ * @param string $toUser
+ *
+ * @return $this
+ */
+ public function toUser($toUser)
+ {
+ $this->toUser = $toUser;
+ return $this;
+ }
+
+ /**
+ * @param string $toHost
+ *
+ * @return $this
+ */
+ public function toHost($toHost)
+ {
+ $this->toHost = $toHost;
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function progress()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function stats()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function recursive()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function verbose()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function checksum()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function archive()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function compress()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function owner()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function group()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function times()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function delete()
+ {
+ $this->option(__FUNCTION__);
+
+ return $this;
+ }
+
+ /**
+ * @param int $seconds
+ *
+ * @return $this
+ */
+ public function timeout($seconds)
+ {
+ $this->option(__FUNCTION__, $seconds);
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function humanReadable()
+ {
+ $this->option('human-readable');
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function wholeFile()
+ {
+ $this->option('whole-file');
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function dryRun()
+ {
+ $this->option('dry-run');
+
+ return $this;
+ }
+
+ /**
+ * @return $this
+ */
+ public function itemizeChanges()
+ {
+ $this->option('itemize-changes');
+
+ return $this;
+ }
+
+ /**
+ * Excludes .git, .svn and .hg items at any depth.
+ *
+ * @return $this
+ */
+ public function excludeVcs()
+ {
+ return $this->exclude([
+ '.git',
+ '.svn',
+ '.hg',
+ ]);
+ }
+
+ /**
+ * @param array|string $pattern
+ *
+ * @return $this
+ */
+ public function exclude($pattern)
+ {
+ return $this->optionList(__FUNCTION__, $pattern);
+ }
+
+ /**
+ * @param string $file
+ *
+ * @return $this
+ *
+ * @throws \Robo\Exception\TaskException
+ */
+ public function excludeFrom($file)
+ {
+ if (!is_readable($file)) {
+ throw new TaskException($this, "Exclude file $file is not readable");
+ }
+
+ return $this->option('exclude-from', $file);
+ }
+
+ /**
+ * @param array|string $pattern
+ *
+ * @return $this
+ */
+ public function includeFilter($pattern)
+ {
+ return $this->optionList('include', $pattern);
+ }
+
+ /**
+ * @param array|string $pattern
+ *
+ * @return $this
+ */
+ public function filter($pattern)
+ {
+ return $this->optionList(__FUNCTION__, $pattern);
+ }
+
+ /**
+ * @param string $file
+ *
+ * @return $this
+ *
+ * @throws \Robo\Exception\TaskException
+ */
+ public function filesFrom($file)
+ {
+ if (!is_readable($file)) {
+ throw new TaskException($this, "Files-from file $file is not readable");
+ }
+
+ return $this->option('files-from', $file);
+ }
+
+ /**
+ * @param string $command
+ *
+ * @return $this
+ */
+ public function remoteShell($command)
+ {
+ $this->option('rsh', "'$command'");
+
+ return $this;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function run()
+ {
+ $command = $this->getCommand();
+
+ return $this->executeCommand($command);
+ }
+
+ /**
+ * Returns command that can be executed.
+ * This method is used to pass generated command from one task to another.
+ *
+ * @return string
+ */
+ public function getCommand()
+ {
+ foreach ((array)$this->fromPath as $from) {
+ $this->option(null, $this->getFromPathSpec($from));
+ }
+ $this->option(null, $this->getToPathSpec());
+
+ return $this->command . $this->arguments;
+ }
+
+ /**
+ * @return string
+ */
+ protected function getFromPathSpec($from)
+ {
+ return $this->getPathSpec($this->fromHost, $this->fromUser, $from);
+ }
+
+ /**
+ * @return string
+ */
+ protected function getToPathSpec()
+ {
+ return $this->getPathSpec($this->toHost, $this->toUser, $this->toPath);
+ }
+
+ /**
+ * @param string $host
+ * @param string $user
+ * @param string $path
+ *
+ * @return string
+ */
+ protected function getPathSpec($host, $user, $path)
+ {
+ $spec = isset($path) ? $path : '';
+ if (!empty($host)) {
+ $spec = "{$host}:{$spec}";
+ }
+ if (!empty($user)) {
+ $spec = "{$user}@{$spec}";
+ }
+
+ return $spec;
+ }
+}