├── .gitignore ├── composer.json ├── LICENSE ├── src ├── Fadion │ └── Maneuver │ │ ├── ManeuverServiceProvider.php │ │ ├── Commands │ │ ├── DeployCommand.php │ │ ├── ListCommand.php │ │ ├── SyncCommand.php │ │ └── RollbackCommand.php │ │ ├── Connection.php │ │ ├── Git.php │ │ ├── Maneuver.php │ │ └── Deploy.php └── config │ └── config.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | composer.phar 3 | composer.lock 4 | .DS_Store 5 | .idea -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fadion/maneuver", 3 | "description": "Easily deploy Laravel projects via FTP or SFTP, using Git for versioning.", 4 | "license": "MIT", 5 | "keywords": ["laravel", "deployment", "git", "ftp", "ssh"], 6 | "authors": [ 7 | { 8 | "name": "Fadion Dashi", 9 | "email": "jonidashi@gmail.com", 10 | "homepage": "http://www.streha.al" 11 | }, 12 | { 13 | "name": "Baki Goxhaj", 14 | "email": "banago@gmail.com", 15 | "homepage": "http://www.wplancer.com" 16 | } 17 | ], 18 | "require": { 19 | "php": ">=5.5.0", 20 | "illuminate/support": "5.*", 21 | "illuminate/console": "5.*", 22 | "illuminate/config": "5.*", 23 | "banago/bridge": "~1.0.8", 24 | "jakeasmith/http_build_url": "~0.1.2" 25 | }, 26 | "autoload": { 27 | "psr-0": { 28 | "Fadion\\Maneuver": "src/" 29 | } 30 | }, 31 | "minimum-stability": "stable" 32 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Fadion Dashi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/Fadion/Maneuver/ManeuverServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 27 | __DIR__ . '/../../config/config.php' => config_path('maneuver.php') 28 | ]); 29 | } 30 | 31 | /** 32 | * Register the service provider. 33 | * 34 | * @return void 35 | */ 36 | public function register() 37 | { 38 | $this->commands([ 39 | Commands\DeployCommand::class, 40 | Commands\ListCommand::class, 41 | Commands\RollbackCommand::class, 42 | Commands\SyncCommand::class, 43 | ]); 44 | } 45 | 46 | /** 47 | * Get the services provided by the provider. 48 | * 49 | * @return array 50 | */ 51 | public function provides() 52 | { 53 | return array('maneuver'); 54 | } 55 | 56 | } -------------------------------------------------------------------------------- /src/config/config.php: -------------------------------------------------------------------------------- 1 | array(), 15 | 16 | /* 17 | |-------------------------------------------------------------------------- 18 | | Default server 19 | |-------------------------------------------------------------------------- 20 | | 21 | | Default server to deploy to when running 'deploy' without any arguments. 22 | | If this options isn't set, deployment will be run to all servers. 23 | | 24 | */ 25 | 'default' => 'development', 26 | 27 | /* 28 | |-------------------------------------------------------------------------- 29 | | Connections List 30 | |-------------------------------------------------------------------------- 31 | | 32 | | Servers available for deployment. Specify one or more connections, such 33 | | as: 'deployment', 'production', 'stating'; each with its own credentials. 34 | | 35 | */ 36 | 37 | 'connections' => array( 38 | 39 | 'development' => array( 40 | 'scheme' => 'ftp', 41 | 'host' => 'yourdevserver.com', 42 | 'user' => 'user', 43 | 'pass' => 'myawesomepass', 44 | 'path' => '/path/to/server/', 45 | 'port' => 21, 46 | 'passive' => true 47 | ), 48 | 49 | 'production' => array( 50 | 'scheme' => 'ftp', 51 | 'host' => 'yourserver.com', 52 | 'user' => 'user', 53 | 'pass' => 'myawesomepass', 54 | 'path' => '/path/to/server/', 55 | 'port' => 21, 56 | 'passive' => true 57 | ), 58 | 59 | ), 60 | 61 | ); -------------------------------------------------------------------------------- /src/Fadion/Maneuver/Commands/DeployCommand.php: -------------------------------------------------------------------------------- 1 | $this->option('server'), 45 | 'repo' => $this->option('repo') 46 | ); 47 | 48 | $maneuver = new Maneuver($options); 49 | $maneuver->mode(Maneuver::MODE_DEPLOY); 50 | $maneuver->start(); 51 | } 52 | catch (Exception $e) { 53 | $this->error($e->getMessage()); 54 | } 55 | } 56 | 57 | /** 58 | * Get the console command arguments. 59 | * 60 | * @return array 61 | */ 62 | protected function getArguments() 63 | { 64 | return array(); 65 | } 66 | 67 | /** 68 | * Get the console command options. 69 | * 70 | * @return array 71 | */ 72 | protected function getOptions() 73 | { 74 | return array( 75 | array('server', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Server to deploy to.', null), 76 | array('repo', 'r', InputOption::VALUE_OPTIONAL, 'Repository to use.', null), 77 | ); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Fadion/Maneuver/Commands/ListCommand.php: -------------------------------------------------------------------------------- 1 | $this->option('server'), 45 | 'repo' => $this->option('repo') 46 | ); 47 | 48 | $maneuver = new Maneuver($options); 49 | $maneuver->mode(Maneuver::MODE_LIST); 50 | $maneuver->start(); 51 | } 52 | catch (Exception $e) { 53 | $this->error($e->getMessage()); 54 | } 55 | } 56 | 57 | /** 58 | * Get the console command arguments. 59 | * 60 | * @return array 61 | */ 62 | protected function getArguments() 63 | { 64 | return array(); 65 | } 66 | 67 | /** 68 | * Get the console command options. 69 | * 70 | * @return array 71 | */ 72 | protected function getOptions() 73 | { 74 | return array( 75 | array('server', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Server to deploy to.', null), 76 | array('repo', 'r', InputOption::VALUE_OPTIONAL, 'Repository to use.', null), 77 | ); 78 | } 79 | 80 | } 81 | -------------------------------------------------------------------------------- /src/Fadion/Maneuver/Connection.php: -------------------------------------------------------------------------------- 1 | servers[$s] = $connections[$s]; 46 | } 47 | } 48 | // Create a single server connection when the 49 | // default server is defined. 50 | elseif (isset($default)) { 51 | if (!isset($connections[$default])) { 52 | throw new Exception(); 53 | } 54 | 55 | $this->servers[$default] = $connections[$default]; 56 | } 57 | // Otherwise add all servers. 58 | else { 59 | $this->servers = $connections; 60 | } 61 | } 62 | 63 | /** 64 | * Returns server list 65 | * 66 | * @return array 67 | */ 68 | public function servers() 69 | { 70 | return $this->servers; 71 | } 72 | 73 | } -------------------------------------------------------------------------------- /src/Fadion/Maneuver/Commands/SyncCommand.php: -------------------------------------------------------------------------------- 1 | $this->option('server'), 45 | 'repo' => $this->option('repo'), 46 | 'sync' => $this->option('commit') 47 | ); 48 | 49 | $maneuver = new Maneuver($options); 50 | $maneuver->mode(Maneuver::MODE_SYNC); 51 | $maneuver->start(); 52 | } 53 | catch (Exception $e) { 54 | $this->error($e->getMessage()); 55 | } 56 | } 57 | 58 | /** 59 | * Get the console command arguments. 60 | * 61 | * @return array 62 | */ 63 | protected function getArguments() 64 | { 65 | return array(); 66 | } 67 | 68 | /** 69 | * Get the console command options. 70 | * 71 | * @return array 72 | */ 73 | protected function getOptions() 74 | { 75 | return array( 76 | array('server', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Server to deploy to.', null), 77 | array('repo', 'r', InputOption::VALUE_OPTIONAL, 'Repository to use.', null), 78 | array('commit', 'c', InputOption::VALUE_OPTIONAL, 'Commit to sync to.', null) 79 | ); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/Fadion/Maneuver/Commands/RollbackCommand.php: -------------------------------------------------------------------------------- 1 | $this->option('server'), 45 | 'repo' => $this->option('repo'), 46 | 'rollback' => $this->option('commit') 47 | ); 48 | 49 | $maneuver = new Maneuver($options); 50 | $maneuver->mode(Maneuver::MODE_ROLLBACK); 51 | $maneuver->start(); 52 | } 53 | catch (Exception $e) { 54 | $this->error($e->getMessage()); 55 | } 56 | } 57 | 58 | /** 59 | * Get the console command arguments. 60 | * 61 | * @return array 62 | */ 63 | protected function getArguments() 64 | { 65 | return array(); 66 | } 67 | 68 | /** 69 | * Get the console command options. 70 | * 71 | * @return array 72 | */ 73 | protected function getOptions() 74 | { 75 | return array( 76 | array('server', 's', InputOption::VALUE_IS_ARRAY | InputOption::VALUE_OPTIONAL, 'Server to deploy to.', null), 77 | array('repo', 'r', InputOption::VALUE_OPTIONAL, 'Repository to use.', null), 78 | array('commit', 'c', InputOption::VALUE_OPTIONAL, 'Commit to rollback to.', null) 79 | ); 80 | } 81 | 82 | } 83 | -------------------------------------------------------------------------------- /src/Fadion/Maneuver/Git.php: -------------------------------------------------------------------------------- 1 | revision = 'HEAD'; 44 | 45 | // A rollback is called, so set the specified 46 | // commit, or to HEAD^ (one commit before). 47 | if (isset($rollback)) { 48 | $this->revision = ($rollback['commit']) ? $rollback['commit'] : 'HEAD^'; 49 | } 50 | 51 | $this->repo = (isset($repo)) ? rtrim($repo, '/') : getcwd(); 52 | 53 | // Check if it's a git repository. 54 | if (!file_exists("$this->repo/.git")) { 55 | throw new Exception("'$this->repo' is not a Git repository."); 56 | } 57 | 58 | $this->subModules(); 59 | 60 | // Load the ignored files array from config. 61 | $ignored = config('maneuver.ignored'); 62 | 63 | if ($ignored) { 64 | foreach ($ignored as $file) { 65 | $this->ignore($file); 66 | } 67 | } 68 | } 69 | 70 | /** 71 | * Runs a Git command. 72 | * 73 | * @param string $command 74 | * @param null|string $repoPath 75 | * @throws Exception if command fails 76 | * @return array 77 | */ 78 | protected function command($command, $repoPath = null) 79 | { 80 | if (!$repoPath) { 81 | $repoPath = $this->repo; 82 | } 83 | 84 | $command = 'git --git-dir="'.$repoPath.'/.git" --work-tree="'.$repoPath.'" '.$command; 85 | 86 | exec(escapeshellcmd($command), $output, $returnStatus); 87 | 88 | if ($returnStatus != 0) { 89 | throw new Exception("The following command was attempted but failed:\r\n$command"); 90 | } 91 | 92 | return $output; 93 | } 94 | 95 | /** 96 | * Checks submodules 97 | * 98 | */ 99 | protected function subModules() 100 | { 101 | $repo = $this->repo; 102 | $output = $this->command('submodule status'); 103 | 104 | if ($output) { 105 | foreach ($output as $line) { 106 | $line = explode(' ', trim($line)); 107 | $this->submodules[] = array( 108 | 'revision' => $line[0], 109 | 'name' => $line[1], 110 | 'path' => $repo.'/'.$line[1] 111 | ); 112 | $this->ignoredFiles[] = $line[1]; 113 | $this->checkSubSubmodules($repo, $line[1]); 114 | } 115 | } 116 | } 117 | 118 | /** 119 | * Checks submodules of submodules 120 | * 121 | * @param string $repo 122 | * @param string $name 123 | */ 124 | protected function checkSubSubmodules($repo, $name) 125 | { 126 | $output = $this->command('submodule foreach git submodule status'); 127 | 128 | if ($output) { 129 | foreach ($output as $line) { 130 | $line = explode(' ', trim($line)); 131 | 132 | if (trim($line[0]) == 'Entering') continue; 133 | 134 | $this->submodules[] = array( 135 | 'revision' => $line[0], 136 | 'name' => $name.'/'.$line[1], 137 | 'path' => $repo.'/'.$name.'/'.$line[1] 138 | ); 139 | $this->ignoredFiles[] = $name.'/'.$line[1]; 140 | } 141 | } 142 | } 143 | 144 | /** 145 | * Gets files from the diff between revisions 146 | * 147 | * @param $revision 148 | * @return mixed 149 | */ 150 | public function diff($revision) 151 | { 152 | if (! $revision) { 153 | return $this->command('ls-files'); 154 | } 155 | 156 | return $this->command("diff --name-status --no-renames {$revision}... {$this->revision}"); 157 | } 158 | 159 | /** 160 | * Gets files from work tree 161 | * 162 | * @return mixed 163 | */ 164 | public function files() 165 | { 166 | return $this->command('ls-files'); 167 | } 168 | 169 | /** 170 | * Gets the current revision hash 171 | * 172 | * @return string 173 | */ 174 | public function localRevision() 175 | { 176 | return $this->command('rev-parse HEAD'); 177 | } 178 | 179 | /** 180 | * Rolls back revision 181 | * 182 | * @return string 183 | */ 184 | public function rollback() 185 | { 186 | return $this->command("checkout {$this->revision}"); 187 | } 188 | 189 | /** 190 | * Reverts to master 191 | * 192 | * @return string 193 | */ 194 | public function revertToMaster() 195 | { 196 | return $this->command('checkout master'); 197 | } 198 | 199 | /** 200 | * Adds files to ignore list 201 | * 202 | * @param $file 203 | */ 204 | protected function ignore($file) 205 | { 206 | $this->ignoredFiles[] = $file; 207 | } 208 | 209 | /** 210 | * Getter for $this->revision 211 | * 212 | * @return null|string 213 | */ 214 | public function getRevision() 215 | { 216 | return $this->revision; 217 | } 218 | 219 | /** 220 | * Getter for $this->repo 221 | * 222 | * @return string 223 | */ 224 | public function getRepo() 225 | { 226 | return $this->repo; 227 | } 228 | 229 | /** 230 | * Setter for $this->repo 231 | * 232 | * @param string $value 233 | * @return string 234 | */ 235 | public function setRepo($value) 236 | { 237 | return $this->repo = $value; 238 | } 239 | 240 | /** 241 | * Getter for $this->ignoredFiles 242 | * 243 | * @return array 244 | */ 245 | public function getIgnored() 246 | { 247 | return $this->ignoredFiles; 248 | } 249 | 250 | /** 251 | * Getter for $this->submodules 252 | * 253 | * @return array 254 | */ 255 | public function getSubModules() 256 | { 257 | return $this->submodules; 258 | } 259 | 260 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Maneuver 2 | 3 | A Laravel package that makes deployment as easy as it has never been. It uses Git to read file changes and deploys to your server(s) via FTP or SFTP. **Why Git?** Because anyone should already version their files and if they do, it's almost certain they're using Git. **Why FTP?** Because it is the easiest transport protocol to implement and use. 4 | 5 | It is dead-simple to use! Add your servers in the config and just run one command. That's it! Anything else will be handled automatically. Oh, and it even supports Git SubModules and Sub-SubModules. Isn't that neat? 6 | 7 | Maneuver is very tightly coupled to [PHPloy](https://Github.com/banago/PHPloy), a CLI tool written in PHP that can deploy any project, not just Laravel apps. 8 | 9 | ![maneuver](https://f.cloud.github.com/assets/374519/2333156/e0198082-a465-11e3-8fe6-f9f306597f8a.gif) 10 | 11 | ## Why? 12 | 13 | There are plenty of fantastic tools for deployment: Capistrano, Rocketeer or Envoy (Laravel's own ssh task runner), to name a few. They get the job done, probably in a more elegant way. So why use this approach? 14 | 15 | While any sane developer uses Git for version control, not anyone of them knows or bothers to understand how you can exploit Git to deploy. The point is that Git's domain isn't deployment, so setting it up to push to a remote repo and trigger hooks to transfer the work tree can be a tedious task. Worth mentioning is that there are still projects hosted on shared hosts, where setting up a remote Git repo may not be possible. 16 | 17 | Maneuver solves these problems with a very simple approach. It takes the best of version control and combines it with file transfer, without needing anything special on the server. Developers have used FTP for decades to selectively upload files, in a very time consuming and error-prone process. Now they can use an automatic tool that needs only a few minutes to get started. 18 | 19 | ## Installation 20 | 21 | First identify the version of Maneuver you need based on your Laravel installation. You'll want to use that specific version in the step below. 22 | 23 | | Laravel Version | Maneuver Version | 24 | | ------------- | ------------- | 25 | | Laravel 4 | ~1.0 | 26 | | Laravel 5.0 - 5.4 | 2.0.* | 27 | | Laravel 5.5 | 2.1.* | 28 | 29 | 1. Add the package to your composer.json file and run `composer update`: 30 | 31 | ```json 32 | { 33 | "require": { 34 | "fadion/maneuver": "2.1.*" 35 | } 36 | } 37 | ``` 38 | 39 | 2. Add `Fadion\Maneuver\ManeuverServiceProvider` to your `config/app.php` file, inside the `providers` array. 40 | 41 | 3. Publish the package's config with `php artisan vendor:publish`, so you can easily modify it in: `config/maneuver.php`. 42 | 43 | ## Configuration 44 | 45 | The first step is to add servers in the configuration file. If you followed step 3 above, you'll find it in `config/maneuver.php`. 46 | 47 | Add one or more servers in the `connections` array, providing a unique, recognizable name for each. Credentials should obviously be entered too. Optionally, specify a default server for deployment, by entering the server's name in the `default` option. Changes will be deployed to that server if not overriden. In case you leave the `default` option empty, deployment will be run to all the servers. 48 | 49 | Don't forget to set the `scheme` for your servers to either `ftp` or `ssh` for SFTP. 50 | 51 | ## Usage 52 | 53 | You'll use Maneuver from the command line, in the same way you run other `artisan` commands. Even if the terminal feels like an obscure environment that you can't wrap your head around, I assure it'll be a piece of cake. 54 | 55 | From now on I'll assume that you already have a local Git repository and that you've commited some changes. Maneuver uses information from Git to get file changes and it won't work if there's no Git repository. Also, I'll assume you've opened a terminal (command prompt or whatever your OS calls it) and are located in the root directory of your Laravel app. 56 | 57 | ### Deployment 58 | 59 | The command you'll be using all the time, mostly without any arguments, is: 60 | 61 | php artisan deploy 62 | 63 | That command will read the Git repo, build a list of files to upload and/or delete and start transferring them via FTP. If you've specified a `default` server in the config file, it will push to it, otherwise it will push to all the servers you've defined. It's also quite verbose, printing useful information on each step of the process. 64 | 65 | ### Servers 66 | 67 | Instead of pushing to the default server, you can deploy to one or more servers of your choice, by passing arguments to the `deploy` command. Supposing we have defined a development, staging and production server, with 'development' being the default, we can deploy only to 'staging' with: 68 | 69 | php artisan deploy --server=staging 70 | 71 | By passing multiple options, we can tell it to deploy to both 'staging' and 'production' server: 72 | 73 | php artisan deploy --server=staging --server=production 74 | 75 | There's even a shortcut option: 76 | 77 | php artisan deploy -s staging -s production 78 | 79 | ### List Changed Files 80 | 81 | For convenience, you can view a list of changed files since your last deployment: 82 | 83 | php artisan deploy:list 84 | 85 | As in the `deploy` command, it will list the changed files of your default server. You can pass the server options here to: 86 | 87 | php artisan deploy:list --server=staging 88 | 89 | As you've guessed, running this command will still connect to your server(s), but no uploads will be done. It just compares revisions and lists those changed files. 90 | 91 | ### Rollback 92 | 93 | Rolling back is a facility, which does the heavy lifting for you by moving temporarily to a previous commit and deploying those files. After the run, the Git repo will be reverted as it was before the command. Use it as a quick way to rollback your files in the server, but not as a way to modify your Git repo. 94 | 95 | To rollback to the previous commit: 96 | 97 | php artisan deploy:rollback 98 | 99 | If you want to rollback to a specific commit: 100 | 101 | php artisan deploy:rollback --commit= 102 | 103 | ### Remote Revision File 104 | 105 | To achieve synchronization, Maneuver will store a `.revision` file in your server(s), which contains the hash of the latest deployment commit. Deleting that file will trigger a fresh deployment and all your files will be transfered again. Editing it's contents should be avoided, otherwise very strange things may happen. 106 | 107 | ### Syncing the Remote Revision File 108 | 109 | The remote revision file is handled automatically and it's generally the required behaviour. For those cases when you'll need to update it's contents, you can sync it with the current local revision or a commit hash of your choice. 110 | 111 | Sync to the current local revision: 112 | 113 | php artisan deploy:sync 114 | 115 | Sync to a specific commit: 116 | 117 | php artisan deploy:sync --commit= 118 | 119 | Running the `sync` command will connect to your server(s) and update the `.revision` file, but no other uploads will be made. 120 | -------------------------------------------------------------------------------- /src/Fadion/Maneuver/Maneuver.php: -------------------------------------------------------------------------------- 1 | null, 'repo' => null, 'rollback' => null, 'sync' => null); 66 | $options = array_merge($defaults, $options); 67 | 68 | $this->optServer = $options['server']; 69 | $this->optRepo = $options['repo']; 70 | $this->optRollback = $options['rollback']; 71 | $this->optSyncCommit = $options['sync']; 72 | } 73 | 74 | /** 75 | * Sets mode 76 | * 77 | * @param string $mode 78 | */ 79 | public function mode($mode) 80 | { 81 | $this->mode = $mode; 82 | } 83 | 84 | /** 85 | * Starts the Maneuver 86 | */ 87 | public function start() 88 | { 89 | // Get server list. 90 | $connection = new Connection($this->optServer); 91 | $servers = $connection->servers(); 92 | 93 | $rollback = null; 94 | 95 | // When in rollback mode, get the commit. 96 | if ($this->mode == self::MODE_ROLLBACK) { 97 | $rollback = array('commit' => $this->optRollback); 98 | } 99 | 100 | // Init the Git object with the repo and 101 | // rollback option. 102 | $git = new Git($this->optRepo, $rollback); 103 | 104 | // There may be one or more servers, but in each 105 | // case it's build as an array. 106 | foreach ($servers as $name => $credentials) { 107 | try { 108 | $options = isset($credentials['options']) ? $credentials['options'] : array(); 109 | 110 | // Connect to the server using the selected 111 | // scheme and options. 112 | $bridge = new Bridge(http_build_url('', $credentials), $options); 113 | } 114 | catch (Exception $e) { 115 | print "Oh snap: {$e->getMessage()}"; 116 | continue; 117 | } 118 | 119 | $deploy = new Deploy($git, $bridge, $credentials); 120 | 121 | print "\r\n+ --------------- § --------------- +"; 122 | print "\n» Server: $name"; 123 | 124 | // Sync mode. Write revision and close the 125 | // connection, so no other files are uploaded. 126 | if ($this->mode == self::MODE_SYNC) { 127 | $deploy->setSyncCommit($this->optSyncCommit); 128 | $deploy->writeRevision(); 129 | 130 | print "\n √ Synced local revision file to remote"; 131 | print "\n+ --------------- √ --------------- +\r\n"; 132 | 133 | continue; 134 | } 135 | 136 | // Rollback to the specified commit. 137 | if ($this->mode == self::MODE_ROLLBACK) { 138 | print "\n« Rolling back "; 139 | $git->rollback(); 140 | } 141 | 142 | $dirtyRepo = $this->push($deploy); 143 | $dirtySubmodules = false; 144 | 145 | // Check if there are any submodules. 146 | if ($git->getSubModules()) { 147 | foreach ($git->getSubModules() as $submodule) { 148 | // Change repo. 149 | $git->setRepo($submodule['path']); 150 | 151 | // Set submodule name. 152 | $deploy->setIsSubmodule($submodule['name']); 153 | 154 | print "\n» Submodule: " . $submodule['name']; 155 | 156 | $dirtySubmodules = $this->push($deploy); 157 | } 158 | } 159 | 160 | // Files are uploaded or deleted, for the main 161 | // repo or submodules. 162 | if (($dirtyRepo or $dirtySubmodules)) { 163 | if ($this->mode == self::MODE_DEPLOY or $this->mode == self::MODE_ROLLBACK) { 164 | // Write latest revision to server. 165 | $deploy->writeRevision(); 166 | } 167 | } 168 | else { 169 | print "\n» Nothing to do."; 170 | } 171 | 172 | print "\n+ --------------- √ --------------- +\r\n"; 173 | 174 | // On rollback mode, revert to master. 175 | if ($this->mode == self::MODE_ROLLBACK) { 176 | $git->revertToMaster(); 177 | } 178 | } 179 | } 180 | 181 | /** 182 | * Handles the upload and delete processes 183 | * 184 | * @param \Fadion\Maneuver\Deploy $deploy 185 | * @return bool 186 | */ 187 | public function push($deploy) 188 | { 189 | // Compare local revision to the remote one, to 190 | // build files to upload and delete. 191 | $message = $deploy->compare(); 192 | print $message; 193 | print "\n+ --------------- + --------------- +"; 194 | 195 | $dirty = false; 196 | 197 | $filesToUpload = $deploy->getFilesToUpload(); 198 | $filesToDelete = $deploy->getFilesToDelete(); 199 | 200 | if ($filesToUpload) { 201 | foreach ($filesToUpload as $file) { 202 | // On list mode, just print the file. 203 | if ($this->mode == self::MODE_LIST) { 204 | print "\n√ \033[0;37m{$file}\033[0m \033[0;32mwill be uploaded\033[0m"; 205 | continue; 206 | } 207 | 208 | $output = $deploy->upload($file); 209 | 210 | // An upload procedure may have more than one 211 | // output message (uploaded file, created dir, etc). 212 | foreach ($output as $message) { 213 | print "\n" . $message; 214 | } 215 | } 216 | } 217 | 218 | if ($filesToDelete) { 219 | foreach ($filesToDelete as $file) { 220 | // On list mode, just print the file. 221 | if ($this->mode == self::MODE_LIST) { 222 | print "\n× \033[0;37m{$file}\033[0m \033[0;31mwill be removed\033[0m"; 223 | continue; 224 | } 225 | 226 | print "\n" . $deploy->delete($file); 227 | } 228 | } 229 | 230 | // Files were uploaded or deleted, so mark 231 | // it as dirty. 232 | if ($filesToUpload or $filesToDelete) { 233 | $dirty = true; 234 | } 235 | 236 | return $dirty; 237 | } 238 | 239 | } 240 | -------------------------------------------------------------------------------- /src/Fadion/Maneuver/Deploy.php: -------------------------------------------------------------------------------- 1 | git = $git; 68 | $this->bridge = $bridge; 69 | $this->ignoredFiles = $git->getIgnored(); 70 | $this->server = $server; 71 | } 72 | 73 | /** 74 | * Compares local revision to the remote one and 75 | * builds files to upload and delete 76 | * 77 | * @throws Exception if unknown git diff status 78 | * @return string 79 | */ 80 | public function compare() 81 | { 82 | $remoteRevision = null; 83 | $filesToUpload = array(); 84 | $filesToDelete = array(); 85 | 86 | // The revision file goes inside the submodule. 87 | if ($this->isSubmodule) { 88 | $this->revisionFile = $this->isSubmodule . '/' . $this->revisionFile; 89 | } 90 | 91 | if ($this->bridge->exists($this->revisionFile)) { 92 | $remoteRevision = $this->bridge->get($this->revisionFile); 93 | 94 | $message = "\r\n» Taking it from '" . substr($remoteRevision, 0, 7) . "'"; 95 | } else { 96 | $message = "\r\n» Fresh deployment - grab a coffee"; 97 | } 98 | 99 | // A remote version exists. 100 | if ($remoteRevision) { 101 | // Get the files from the diff. 102 | $output = $this->git->diff($remoteRevision); 103 | 104 | foreach ($output as $line) { 105 | // Added, changed or modified. 106 | if ($line[0] == 'A' or $line[0] == 'C' or $line[0] == 'M') { 107 | $filesToUpload[] = trim(substr($line, 1)); 108 | } 109 | // Deleted. 110 | elseif ($line[0] == 'D') { 111 | $filesToDelete[] = trim(substr($line, 1)); 112 | } 113 | // Unknown status. 114 | else { 115 | throw new Exception("Unknown git-diff status: {$line[0]}"); 116 | } 117 | } 118 | } 119 | // No remote version. Get all files. 120 | else { 121 | $filesToUpload = $this->git->files(); 122 | } 123 | 124 | // Remove ignored files from the list of uploads. 125 | $filesToUpload = array_diff($filesToUpload, $this->ignoredFiles); 126 | 127 | $this->filesToUpload = $filesToUpload; 128 | $this->filesToDelete = $filesToDelete; 129 | 130 | return $message; 131 | } 132 | 133 | /** 134 | * Getter for $this->filesToUpload 135 | * 136 | * @return array 137 | */ 138 | public function getFilesToUpload() 139 | { 140 | return $this->filesToUpload; 141 | } 142 | 143 | /** 144 | * Getter for $this->filesToDelete 145 | * 146 | * @return array 147 | */ 148 | public function getFilesToDelete() 149 | { 150 | return $this->filesToDelete; 151 | } 152 | 153 | /** 154 | * Getter for $this->isSubmodule 155 | * 156 | * @return mixed 157 | */ 158 | public function getIsSubmodule() 159 | { 160 | return $this->isSubmodule; 161 | } 162 | 163 | /** 164 | * Setter for $this->isSubmodule 165 | * 166 | * @param string $value 167 | */ 168 | public function setIsSubmodule($value) 169 | { 170 | $this->isSubmodule = $value; 171 | } 172 | 173 | /** 174 | * Sets the commit to sync revision 175 | * file to 176 | * 177 | * @param string $value 178 | */ 179 | public function setSyncCommit($value) 180 | { 181 | $this->syncCommit = $value; 182 | } 183 | 184 | /** 185 | * Uploads file 186 | * 187 | * @param string $file 188 | * @return array 189 | */ 190 | public function upload($file) 191 | { 192 | if ($this->isSubmodule) { 193 | $file = $this->isSubmodule.'/'.$file; 194 | } 195 | 196 | $dir = explode('/', dirname($file)); 197 | $path = ''; 198 | $pathThatExists = null; 199 | $output = array(); 200 | 201 | // Skip basedir or parent. 202 | if ($dir[0] != '.' and $dir[0] != '..') { 203 | // Iterate through directory pieces. 204 | for ($i = 0, $count = count($dir); $i < $count; $i++) { 205 | $path .= $dir[$i].'/'; 206 | 207 | if (!isset($pathThatExists[$path])) { 208 | $origin = $this->bridge->pwd(); 209 | 210 | // The directory doesn't exist. 211 | if (! $this->bridge->exists($path)) { 212 | // Attempt to create the directory. 213 | $this->bridge->mkdir($path); 214 | $output[] = "Created directoy '$path'.'"; 215 | } 216 | // The directory exists. 217 | else { 218 | $this->bridge->cd($path); 219 | } 220 | 221 | $pathThatExists[$path] = true; 222 | $this->bridge->cd($origin); 223 | } 224 | } 225 | } 226 | 227 | $uploaded = false; 228 | $attempts = 1; 229 | 230 | // Loop until $uploaded becomes a valid 231 | // resource. 232 | while (!$uploaded) { 233 | // Attempt to upload the file 10 times 234 | // and exit if it fails. 235 | if ($attempts == 10) { 236 | $output[] = "Tried to upload $file 10 times, and failed 10 times. Something is wrong, so I'm going to stop executing now."; 237 | return $output; 238 | } 239 | 240 | $data = file_get_contents($file); 241 | $uploaded = $this->bridge->put($data, $file); 242 | 243 | if (!$uploaded) { 244 | $attempts++; 245 | } 246 | } 247 | 248 | $output[] = "√ \033[0;37m{$file}\033[0m \033[0;32muploaded\033[0m"; 249 | 250 | return $output; 251 | } 252 | 253 | /** 254 | * Delete file 255 | * 256 | * @param $file 257 | * @return string 258 | */ 259 | public function delete($file) 260 | { 261 | $this->bridge->rm($file); 262 | 263 | return "× \033[0;37m{$file}\033[0m \033[0;31mremoved\033[0m"; 264 | } 265 | 266 | /** 267 | * Writes latest revision to the remote 268 | * revision file 269 | * 270 | * @throws Exception if can't update revision file 271 | */ 272 | public function writeRevision() 273 | { 274 | if ($this->syncCommit) { 275 | $localRevision = $this->syncCommit; 276 | } else { 277 | $localRevision = $this->git->localRevision()[0]; 278 | } 279 | 280 | try { 281 | $this->bridge->put($localRevision, $this->revisionFile); 282 | } 283 | catch (Exception $e) { 284 | throw new Exception("Could not update the revision file on server: {$e->getMessage()}"); 285 | } 286 | } 287 | 288 | } --------------------------------------------------------------------------------