├── .gitignore ├── .gitmodules ├── CHANGELOG.md ├── README.md ├── bin └── pug ├── lib ├── cleanup.php ├── commands │ ├── _global.php │ ├── add.php │ ├── disable.php │ ├── enable.php │ ├── install.php │ ├── remove.php │ ├── rename.php │ ├── show.php │ ├── update.php │ └── upgrade.php ├── config.php └── pug │ ├── Autoloader.php │ ├── DependencyManager │ ├── CocoaPods.php │ ├── Composer.php │ └── IDependencyManager.php │ ├── InvalidDirectoryException.php │ ├── MissingSourceControlException.php │ ├── Project.php │ └── Pug.php ├── pug └── test ├── commands ├── PugTestCase.php ├── addTest.php ├── disableTest.php ├── enableTest.php ├── removeTest.php ├── renameTest.php └── showTest.php └── fixtures └── pugfiles ├── .pug-disabled ├── .pug-enabled ├── .pug-groups └── .pug-new /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .cookies 3 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "vendor/huxtable/core"] 2 | path = vendor/huxtable/core 3 | url = https://github.com/huxtable/core.git 4 | [submodule "vendor/huxtable/cli"] 5 | path = vendor/huxtable/cli 6 | url = https://github.com/huxtable/cli.git 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | 3 | All notable changes to Pug will be documented in this file (beginning with v0.5 😅). 4 | 5 | ## [Unreleased] 6 | 7 | ## [0.7.3] - 2017-08-12 8 | ### Added 9 | - Specify alternative storage location using `PUGFILE` environment variable 10 | - `--no-color` option to commands which output project listing 11 | - `[-y|--yes|--assume-yes]` option for `remove` command to run non-interactively 12 | 13 | ### Fixed 14 | - PHP 7 reserved-name collision — [#28](https://github.com/ashur/pug/issues/28) 15 | 16 | ## [0.7.2] - 2016-12-23 17 | ### Added 18 | - Support for displaying project metadata 19 | - Support for cloning new projects 20 | 21 | ## [0.7.1] - 2016-11-14 22 | ### Fixed 23 | - Iterate through post-update submodule inventory during state restoration 24 | - No longer overwrite invalid path with `false` 25 | - Silently skip projects with file as path 26 | 27 | ## [0.7.0] - 2016-11-14 28 | ### Added 29 | - `pug install` 30 | 31 | ## [0.6.0] - 2016-08-10 32 | ### Added 33 | - Namespaces 34 | - Command support for namespaces (aka groups): enable, disable, remove, update 35 | - Rename projects 36 | 37 | ### Fixed 38 | - Always rebase, not just when changes are fetched 39 | 40 | ### Removed 41 | - Subversion support 42 | 43 | ## [0.5.0] - 2016-08-06 44 | ### Added 45 | - `pug upgrade` 46 | - ./pug -> ./bin/pug symlink 47 | - Self cleanup 48 | - `pug.update.rebase true` 49 | 50 | ### Fixed 51 | - Fix `pug update` to always return submodules to their original states 52 | 53 | ### Deprecated 54 | - Subversion support 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ![Quickly update Git and Subversion projects and their dependencies with a single command.](http://pug.sh.s3.amazonaws.com/pug3.png) 2 | 3 | One command is all you need to update local repositories and their submodules. If a project is using [CocoaPods](https://cocoapods.org) or [Composer](https://getcomposer.org) to manage its dependencies, Pug will automatically update those, too. 4 | 5 | ## Contents 6 | 7 | 1. [Installation](#installation) 8 | 1. [Basics](#basics) 9 | 1. [Update](#update) 10 | 1. [Configuration](#configuration) 11 | 1. [Tips](#-tips) 12 | 1. [Help](#help) 13 | 14 | ## Installation 15 | 16 | ### Homebrew 17 | 18 | Installing Fig with [Homebrew](https://brew.sh) is a snap: 19 | 20 | ``` 21 | $ brew tap ashur/pug 22 | $ brew install pug 23 | ``` 24 | 25 | That's it! Installing future updates is also a cinch: 26 | 27 | ``` 28 | $ brew upgrade pug 29 | ``` 30 | 31 | ### Git 32 | 33 | You can also install Pug using Git. First, clone this repository: 34 | 35 | ``` 36 | $ cd ~/tools 37 | $ git clone --recursive https://github.com/ashur/pug.git 38 | ``` 39 | 40 | > 🎉 **New in v0.7** 41 | 42 | Next, run the `install` command and include a directory that's already on your `$PATH`: 43 | 44 | ``` 45 | $ pug/bin/pug install /usr/local/bin 46 | Linked to '/usr/local/bin/pug' 47 | ``` 48 | 49 | This will symlink the `pug` executable, letting you run `pug` from anywhere on the command line: 50 | 51 | ``` 52 | $ cd ~/Desktop/ 53 | $ pug --version 54 | pug version 0.7.0 55 | ``` 56 | 57 | ### Requirements 58 | 59 | Pug requires PHP 5.4 or greater 60 | 61 | ### Upgrading 62 | 63 | > 🎉 **New in v0.5** 64 | 65 | If you're upgrading from **v0.5** or later, use the built-in command to fetch the latest version: 66 | 67 | ``` 68 | $ pug upgrade 69 | ``` 70 | 71 | If you're upgrading from **v0.4** or earlier, the best way to handle all the submodules and fiddly bits is with Pug itself: 72 | 73 | ``` 74 | $ cd ~/tools/pug 75 | $ pug up 76 | ``` 77 | 78 | > 💡 **Tip** — Not sure which version you're running? 79 | > 80 | > ``` 81 | > $ pug --version 82 | > pug version 0.7.0 83 | > ``` 84 | > 85 | > Nice. 😁 86 | 87 | 88 | ## Basics 89 | 90 | After you've installed Pug, try updating a local Git repository at any path: 91 | 92 | ``` 93 | $ pug update ~/Developer/tapas 94 | Updating '/Users/ashur/Developer/tapas'... 95 | 96 | • Pulling... 97 | > Already up-to-date. 98 | 99 | • Updating submodules... done. 100 | ``` 101 | 102 | By default, `pug update` performs two operations on the repository: 103 | 104 | * `git pull` 105 | * `git submodule update` 106 | 107 | > 🔬 [Learn more](#update) about the specifics of what Pug does during an update. You can re-configure the default behavior on a global or per-repository basis. See [Configuration](#configuration) for more information. 108 | 109 | Using Pug to update repositories at an arbitrary path is nice, but projects make things even easier. 110 | 111 | 112 | ### Projects 113 | 114 | First, let's add a repository to our list of tracked projects: 115 | 116 | ``` 117 | $ pug add plank ~/Developer/plank 118 | * plank 119 | ``` 120 | 121 | Now we can grab updates using the project name instead of the full path: 122 | 123 | ``` 124 | $ pug update plank 125 | ``` 126 | 127 | That's nicer! Let's add a few more projects: 128 | 129 | ``` 130 | $ pug show 131 | * plank 132 | * prompt 133 | * transmit 134 | ``` 135 | 136 | With a single command, we can update multiple projects _and_ their submodules. 137 | 138 | ``` 139 | $ pug update all 140 | ``` 141 | 142 | ### Enable/Disable 143 | 144 | Need to focus on a subset of your projects for a while? Disable anything you don't need: 145 | 146 | ``` 147 | $ pug disable prompt 148 | * plank 149 | prompt 150 | * transmit 151 | ``` 152 | 153 | Pug will hold on to the project definition, but skip it when you `update all`: 154 | 155 | ``` 156 | $ pug update all 157 | Updating 'plank'... 158 | 159 | • Pulling... 160 | > Already up-to-date. 161 | 162 | Updating 'transmit'... 163 | 164 | • Pulling... 165 | > Already up-to-date. 166 | 167 | • Updating submodules... done. 168 | 169 | ``` 170 | 171 | 172 | ### Groups 173 | 174 | > 🎉 **New in v0.6** 175 | 176 | As the list of tracked projects grows, it can get harder to stay organized: 177 | 178 | ``` 179 | $ pug show 180 | * ansible 181 | * cios 182 | * dotfiles 183 | * mlib 184 | plank 185 | prompt 186 | * tapas 187 | * tios 188 | * transmit 189 | * zoo 190 | ``` 191 | 192 | Groups help keep things nice and tidy. To add a new project to a group, use the `/` naming pattern: 193 | 194 | ``` 195 | $ pug add mac/coda ~/Developer/Coda 196 | ``` 197 | 198 | To move an existing project into a group, just rename it: 199 | 200 | ``` 201 | $ pug rename ansible sysops/ansible 202 | ``` 203 | 204 | Much better: 205 | 206 | ``` 207 | $ pug show 208 | * bots/mlib 209 | * bots/zoo 210 | * dotfiles 211 | * ios/coda 212 | ios/prompt 213 | * ios/transmit 214 | * mac/coda 215 | * mac/transmit 216 | * sysops/ansible 217 | web/plank 218 | * web/tapas 219 | ``` 220 | 221 | In addition to keeping projects organized, we can also perform operations on groups just like individual projects. Done with `ios` for a while? Disable all the projects in that group: 222 | 223 | ``` 224 | $ pug disable ios 225 | ``` 226 | 227 | Want to update just the projects in your `bots` group? Simple! 228 | 229 | ``` 230 | $ pug update bots 231 | ``` 232 | 233 | > 💡 **Tip** — Add `--all` to update disabled projects in the group as well 234 | 235 | 236 | ## Update 237 | 238 | It's important for you to know what Pug is doing on your behalf during `pug update`. In order of operations: 239 | 240 | ``` 241 | git pull 242 | git submodule update --init --recursive 243 | ``` 244 | 245 | If the `pug.update.rebase` configuration option is set to `true`, Pug will instead run: 246 | 247 | ``` 248 | git fetch 249 | git rebase 250 | git submodule update --init --recursive 251 | ``` 252 | 253 | > 🔬 [Learn more](#configuration) about how to configure `pug update` to suit your needs 254 | 255 | ### Dependency Managers 256 | 257 | If Pug detects CocoaPods, it will try to determine if an update is necessary. If so: 258 | 259 | ``` 260 | pod install 261 | ``` 262 | 263 | If Pug detects Composer and determines an update is necessary: 264 | 265 | ``` 266 | composer update 267 | ``` 268 | 269 | To force dependency updates: 270 | 271 | ``` 272 | $ pug update --force 273 | ``` 274 | 275 | ### Submodule State Restoration 276 | 277 | > 🎉 **New in v0.5** 278 | 279 | In previous versions, submodules were always left checked out on a detached HEAD after `pug update`. If you were doing development on a submodule, checking the submodule back out to its original branch and pulling down changes manually was a hassle. 280 | 281 | With state restoration, if a submodule is checked out to a branch, Pug now returns it to its previous state and automatically `pull`s down any changes from the submodule's remote as well: 282 | 283 | ``` 284 | $ pug update tapas 285 | Updating 'tapas'... 286 | 287 | • Pulling... 288 | > From github.com:ashur/corpora 289 | > 7b4a17c..e1caf08 master -> origin/master 290 | > Updating 339cea5..57c314f 291 | > Fast-forward 292 | > corpora | 2 +- 293 | > 1 file changed, 1 insertion(+), 1 deletion(-) 294 | 295 | • Updating submodules... 296 | > Submodule path 'corpora': checked out 'e1caf08eac44a149b14e4f2bbc4eb12ba6a4e6e4' 297 | % Submodule path 'corpora': checked out 'master' 298 | % Submodule path 'corpora': pulling 'master'... done. 299 | ``` 300 | 301 | > **Note** — If a submodule is checked out on a detached HEAD prior to the update, `pug update` leaves it that way. 302 | 303 | 304 | ## Configuration 305 | 306 | Pug supports a few configuration options by piggybacking Git's own `config` command. They can be set either globally (using the `--global` flag) or on a per-project basis. 307 | 308 | For example, we can change the global `pug update` behavior to always automatically stash changes: 309 | 310 | ``` 311 | $ git config --global pug.update.stash true 312 | ``` 313 | 314 | and still keep the default no-stash behavior where we need to: 315 | 316 | ``` 317 | $ cd ~/Developer/tapas 318 | $ git config pug.update.stash false 319 | ``` 320 | 321 | ### Options 322 | 323 | ⚙ pug.update.**rebase** 324 | 325 | > _boolean_ — When **true**, `pug update` will perform `git fetch` and `git rebase` instead of `git pull`. 326 | > 327 | > Default value is **false** 328 | 329 | > 🎉 **New in v0.5** 330 | 331 | ⚙ pug.update.**stash** 332 | 333 | > _boolean_ — When **true**, automatically `stash` any changes in the active project before `pull`-ing, then pop the stack afterward 334 | > 335 | > Default value is **false** 336 | 337 | ⚙ pug.update.**submodules** 338 | 339 | > _boolean_ — Whether to update submodules during `pug update` 340 | > 341 | > Default value is **true** 342 | 343 | 344 | 345 | ## 💡 Tips 346 | 347 | ### Save the "date" 348 | 349 | Save yourself a few keystrokes every `update`: 350 | 351 | ``` 352 | $ pug up 353 | ``` 354 | 355 | 356 | ## Help 357 | 358 | Command-specific help is always available on the command line: 359 | 360 | ``` 361 | $ pug help 362 | usage: pug [--version] [] 363 | 364 | Commands are: 365 | add Start tracking a new project 366 | disable Exclude projects from 'all' updates 367 | enable Include projects in 'all' updates 368 | install Symlink 'pug' to a convenient path 369 | rename Rename an existing project 370 | rm Stop tracking projects 371 | show Show tracked projects 372 | update Fetch project updates 373 | upgrade Fetch the newest version of Pug 374 | 375 | See 'pug help ' to read about a specific command 376 | ``` 377 | -------------------------------------------------------------------------------- /bin/pug: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | $action ) 35 | { 36 | $app->registerCleanupAction( $version, $action ); 37 | } 38 | $app->cleanUpSelf(); 39 | 40 | /* 41 | * Register commands 42 | */ 43 | $fileFilter = new File\Filter(); 44 | $fileFilter 45 | ->setDefaultMethod( $fileFilter::METHOD_INCLUDE ) 46 | ->includeFileExtension( 'php' ); 47 | 48 | $dirCommands = new File\Directory( $pathCommands ); 49 | if( $dirCommands->exists() ) 50 | { 51 | $commandFiles = $dirCommands->children( $fileFilter ); 52 | 53 | foreach( $commandFiles as $commandFile ) 54 | { 55 | $command = include_once( $commandFile ); 56 | if( $command instanceof Huxtable\CLI\Command ) 57 | { 58 | $app->registerCommand( $command ); 59 | } 60 | } 61 | } 62 | 63 | /* 64 | * Timezone 65 | */ 66 | $timezone = 'UTC'; 67 | // Override with value from php.ini if set 68 | if( strlen( $iniTimezone = ini_get( 'date.timezone' ) ) > 0 ) 69 | { 70 | $timezone = $iniTimezone; 71 | } 72 | // Override with value from config.php if set 73 | if( isset( $userConfig['timezone'] ) ) 74 | { 75 | $timezone = $userConfig['timezone']; 76 | } 77 | date_default_timezone_set( $timezone ); 78 | 79 | // Attempt to run the requested command 80 | try 81 | { 82 | $app->run(); 83 | } 84 | catch( Exception $e ) 85 | { 86 | $log = Output::exceptionLog( $e, $argv, 'https://github.com/ashur/pug/issues/new' ); 87 | echo $log; 88 | exit( 1 ); 89 | } 90 | 91 | // Stop application and exit 92 | $app->stop(); 93 | -------------------------------------------------------------------------------- /lib/cleanup.php: -------------------------------------------------------------------------------- 1 | function( $dirPug ) 16 | { 17 | $dirHuxtable = $dirPug->childDir( 'vendor' )->childDir( 'Huxtable' ); 18 | 19 | return array( 20 | /* ./ */ 21 | $dirPug->child( 'config.php' ), 22 | 23 | /* ./vendor/huxtable */ 24 | $dirHuxtable->child( '.gitignore' ), 25 | $dirHuxtable->child( 'README.md' ), 26 | $dirHuxtable->child( 'composer.json' ), 27 | $dirHuxtable->childDir( 'src' ), 28 | ); 29 | }, 30 | ); 31 | -------------------------------------------------------------------------------- /lib/commands/_global.php: -------------------------------------------------------------------------------- 1 | foregroundColor( 'green' ); 30 | } 31 | else 32 | { 33 | $iconEnabled = '*'; 34 | } 35 | 36 | $maxLengthName = 0; 37 | $maxLengthBranch = 0; 38 | 39 | /* Get project values */ 40 | $metadata = []; 41 | foreach( $projects as $project ) 42 | { 43 | $projectMetadata['branch'] = $project->getActiveBranch(); 44 | $projectMetadata['commit'] = $project->getCommitHash(); 45 | $projectMetadata['icon'] = $project->isEnabled() ? $iconEnabled : ' '; 46 | $projectMetadata['name'] = $project->getName(); 47 | $projectMetadata['path'] = str_replace( getenv( 'HOME' ), '~', $project->getPath() ); 48 | 49 | if( strlen( $projectMetadata['name'] ) > $maxLengthName ) 50 | { 51 | $maxLengthName = strlen( $projectMetadata['name'] ); 52 | } 53 | if( strlen( $projectMetadata['branch'] ) > $maxLengthBranch ) 54 | { 55 | $maxLengthBranch = strlen( $projectMetadata['branch'] ); 56 | } 57 | 58 | $metadata[] = $projectMetadata; 59 | } 60 | 61 | /* Generate output */ 62 | foreach( $metadata as $project ) 63 | { 64 | $line = sprintf( 65 | "%s %-{$maxLengthName}s", 66 | $project['icon'], 67 | $project['name'] 68 | ); 69 | 70 | if( $showGit ) 71 | { 72 | $line .= sprintf( " %-{$maxLengthBranch}s %-7s", 73 | $project['branch'], 74 | $project['commit'] 75 | ); 76 | } 77 | 78 | if( $showPath ) 79 | { 80 | if( $useColor ) 81 | { 82 | $projectPath = new CLI\FormattedString( $project['path'] ); 83 | $projectPath->foregroundColor( 'cyan' ); 84 | } 85 | else 86 | { 87 | $projectPath = $project['path']; 88 | } 89 | 90 | $line .= " {$projectPath}"; 91 | } 92 | 93 | $output->line( $line ); 94 | } 95 | 96 | return $output; 97 | } 98 | -------------------------------------------------------------------------------- /lib/commands/add.php: -------------------------------------------------------------------------------- 1 | /] [--url=] 15 | * @alias track 16 | */ 17 | $commandAdd = new CLI\Command( 'add', 'Start tracking a new project', function( $name, $path ) 18 | { 19 | try 20 | { 21 | $dirProject = new File\Directory( $path ); 22 | } 23 | catch( \Exception $e ) 24 | { 25 | throw new CLI\Command\CommandInvokedException( "Couldn't track project. {$e->getMessage()}", 1 ); 26 | } 27 | 28 | /* Clone new project to track */ 29 | $repoURL = $this->getOptionValue( 'url' ); 30 | if( !is_null( $repoURL ) ) 31 | { 32 | if( $dirProject->exists() ) 33 | { 34 | if( count( $dirProject->children() ) > 0 ) 35 | { 36 | throw new CLI\Command\CommandInvokedException( "Couldn't clone repository: '{$path}' already exists and is not an empty directory.", 1 ); 37 | } 38 | } 39 | 40 | echo "Cloning into '{$dirProject}'... " ; 41 | $result = CLI\Shell::exec( "git clone --recursive {$repoURL} {$dirProject}", true, ' > ' ); 42 | 43 | if( $result['exitCode'] === 0 ) 44 | { 45 | echo 'done.' . PHP_EOL; 46 | } 47 | else 48 | { 49 | echo 'failed:' . PHP_EOL . PHP_EOL; 50 | echo $result['output']['formatted'] . PHP_EOL; 51 | 52 | exit( 1 ); 53 | } 54 | } 55 | 56 | if( !$dirProject->exists() ) 57 | { 58 | throw new CLI\Command\CommandInvokedException( "Couldn't track project. Path '{$path}' not found.", 1 ); 59 | } 60 | 61 | $pug = new Pug(); 62 | $project = new Project( $name, $dirProject, true, $dirProject->getCTime() ); 63 | 64 | try 65 | { 66 | $pug->addProject( $project ); 67 | } 68 | catch( \Exception $e ) 69 | { 70 | throw new CLI\Command\CommandInvokedException( "Couldn't track project. {$e->getMessage()}", 1 ); 71 | } 72 | 73 | $useColor = $this->getOptionValue( 'no-color' ) == null; 74 | $output = listProjects( $pug->getProjects(), false, false, $useColor ); 75 | 76 | return $output->flush(); 77 | }); 78 | 79 | /* Options */ 80 | $commandAdd->registerOption( 'no-color' ); 81 | $commandAdd->registerOption( 'url' ); 82 | 83 | /* Aliases */ 84 | $commandAdd->addAlias( 'track' ); 85 | 86 | /* Usage */ 87 | $commandAddUsage = <</] [--url=] 89 | 90 | OPTIONS 91 | --url= 92 | Clone the Git repository at to . 93 | 94 | USAGE; 95 | 96 | $commandAdd->setUsage( $commandAddUsage ); 97 | 98 | return $commandAdd; 99 | -------------------------------------------------------------------------------- /lib/commands/disable.php: -------------------------------------------------------------------------------- 1 | |] 15 | */ 16 | $commandDisable = new CLI\Command( 'disable', 'Exclude projects from \'all\' updates', function( $query ) 17 | { 18 | $pug = new Pug(); 19 | $query = strtolower( $query ); 20 | 21 | try 22 | { 23 | if( $query == 'all' ) 24 | { 25 | $pug->disableAllProjects(); 26 | } 27 | // Is this a namespace? 28 | elseif( $pug->namespaceExists( $query ) ) 29 | { 30 | $pug->disableProjectsInNamespace( $query ); 31 | } 32 | // ...or is this a project? 33 | else 34 | { 35 | $pug->disableProject( $query ); 36 | } 37 | } 38 | catch( \Exception $e ) 39 | { 40 | throw new Command\CommandInvokedException( "No groups or projects match '{$query}'.", 1 ); 41 | } 42 | 43 | $useColor = $this->getOptionValue( 'no-color' ) == null; 44 | $output = listProjects( $pug->getProjects(), false, false, $useColor ); 45 | 46 | return $output->flush(); 47 | }); 48 | 49 | /* Options */ 50 | $commandDisable->registerOption( 'no-color' ); 51 | 52 | $commandDisable->setUsage( 'disable [all||]' ); 53 | 54 | return $commandDisable; 55 | -------------------------------------------------------------------------------- /lib/commands/enable.php: -------------------------------------------------------------------------------- 1 | |] 15 | */ 16 | $commandEnable = new CLI\Command( 'enable', 'Include projects in \'all\' updates', function( $query ) 17 | { 18 | $pug = new Pug(); 19 | $query = strtolower( $query ); 20 | 21 | try 22 | { 23 | if( $query == 'all' ) 24 | { 25 | $pug->enableAllProjects(); 26 | } 27 | // Is this a namespace? 28 | elseif( $pug->namespaceExists( $query ) ) 29 | { 30 | $pug->enableProjectsInNamespace( $query ); 31 | } 32 | // ...or is this a project? 33 | else 34 | { 35 | $pug->enableProject( $query ); 36 | } 37 | } 38 | catch( \Exception $e ) 39 | { 40 | throw new Command\CommandInvokedException( "No groups or projects match '{$query}'.", 1 ); 41 | } 42 | 43 | $useColor = $this->getOptionValue( 'no-color' ) == null; 44 | $output = listProjects( $pug->getProjects(), false, false, $useColor ); 45 | 46 | return $output->flush(); 47 | }); 48 | 49 | /* Options */ 50 | $commandEnable->registerOption( 'no-color' ); 51 | 52 | $commandEnable->setUsage( 'enable [all||]' ); 53 | 54 | return $commandEnable; 55 | -------------------------------------------------------------------------------- /lib/commands/install.php: -------------------------------------------------------------------------------- 1 | 10 | */ 11 | $command = new Command( 'install', 'Symlink \'pug\' to a convenient path', function( $dir ) 12 | { 13 | $pathSource = dirname( dirname( __DIR__ ) ) . '/bin/pug'; 14 | 15 | try 16 | { 17 | $destinationDirectory = new File\Directory( $dir ); 18 | 19 | if( !$destinationDirectory->exists() ) 20 | { 21 | throw new \Exception( "Invalid location: '{$dir}' does not exist" ); 22 | } 23 | } 24 | catch( \Exception $e ) 25 | { 26 | throw new Command\CommandInvokedException( $e->getMessage(), 1 ); 27 | } 28 | 29 | if( !$destinationDirectory->isWritable() ) 30 | { 31 | throw new Command\CommandInvokedException( "Invalid location: You do not have permission to write to '{$destinationDirectory}'" ); 32 | } 33 | 34 | $target = $destinationDirectory->child( 'pug' ); 35 | $source = new File\File( $pathSource ); 36 | 37 | if( !$source->exists() ) 38 | { 39 | throw new Command\CommandInvokedException( "Invalid source: '{$source}' not found", 1 ); 40 | } 41 | 42 | if( $target->exists() || is_link( $target->getPathname() ) ) 43 | { 44 | throw new Command\CommandInvokedException( "Invalid target: '{$target}' already exists", 1 ); 45 | } 46 | 47 | symlink( $source, $target ); 48 | echo "Linked to '{$target}'" . PHP_EOL; 49 | }); 50 | 51 | return $command; 52 | -------------------------------------------------------------------------------- /lib/commands/remove.php: -------------------------------------------------------------------------------- 1 | |] 16 | * @alias remove,untrack 17 | */ 18 | $commandRemove = new CLI\Command('rm', 'Stop tracking projects', function( $query ) 19 | { 20 | $pug = new Pug(); 21 | $query = strtolower( $query ); 22 | 23 | /* Assume "yes" */ 24 | $assumeYes = $this->getOptionValue( 'y' ) == true; 25 | $assumeYes = $assumeYes || $this->getOptionValue( 'yes' ) == true; 26 | $assumeYes = $assumeYes || $this->getOptionValue( 'assume-yes' ) == true; 27 | 28 | try 29 | { 30 | if( $query == 'all' ) 31 | { 32 | if( !$assumeYes ) 33 | { 34 | $didConfirm = strtolower( Input::prompt( 'Are you sure you want to remove all projects from Pug? (y/n)' ) ); 35 | } 36 | if( $assumeYes || $didConfirm == 'y' ) 37 | { 38 | $pug->removeAllProjects(); 39 | } 40 | } 41 | // Is this a namespace? 42 | elseif( $pug->namespaceExists( $query ) ) 43 | { 44 | $namespace = Project::getNormalizedNamespaceString( $query ); 45 | 46 | if( !$assumeYes ) 47 | { 48 | $didConfirm = strtolower( Input::prompt( "Are you sure you want to remove all projects in the '{$namespace}' group? (y/n)" ) ); 49 | } 50 | if( $assumeYes || $didConfirm == 'y' ) 51 | { 52 | $pug->removeProjectsInNamespace( $namespace ); 53 | } 54 | } 55 | // ...or is this a project? 56 | else 57 | { 58 | $pug->removeProject( $query ); 59 | } 60 | } 61 | catch( \Exception $e ) 62 | { 63 | throw new Command\CommandInvokedException( "No groups or projects match '{$query}'.", 1 ); 64 | } 65 | 66 | $useColor = $this->getOptionValue( 'no-color' ) == null; 67 | $output = listProjects( $pug->getProjects(), false, false, $useColor ); 68 | 69 | return $output->flush(); 70 | }); 71 | 72 | /* Options */ 73 | $commandRemove->registerOption( 'no-color' ); 74 | $commandRemove->registerOption( 'y' ); 75 | $commandRemove->registerOption( 'yes' ); 76 | $commandRemove->registerOption( 'assume-yes' ); 77 | 78 | $commandRemove->addAlias( 'remove' ); 79 | $commandRemove->addAlias( 'untrack' ); 80 | 81 | /* Usage */ 82 | $commandRemoveUsage = <<[/]|] [-y|--yes|--assume-yes] 84 | 85 | OPTIONS 86 | -y, --yes, --assume-yes 87 | Automatic yes to prompts. Assume "yes" as answer to all prompts and run 88 | non-interactively. 89 | 90 | USAGE; 91 | 92 | $commandRemove->setUsage( $commandRemoveUsage ); 93 | 94 | return $commandRemove; 95 | -------------------------------------------------------------------------------- /lib/commands/rename.php: -------------------------------------------------------------------------------- 1 | 15 | */ 16 | $commandRename = new CLI\Command( 'rename', 'Rename an existing project', function( $old, $new ) 17 | { 18 | $pug = new Pug(); 19 | 20 | try 21 | { 22 | $pug->renameProject( $old, $new ); 23 | } 24 | catch( \Exception $e ) 25 | { 26 | throw new Command\CommandInvokedException( $e->getMessage(), 1 ); 27 | } 28 | 29 | $useColor = $this->getOptionValue( 'no-color' ) == null; 30 | $output = listProjects( $pug->getProjects(), false, false, $useColor ); 31 | 32 | return $output->flush(); 33 | }); 34 | 35 | /* Options */ 36 | $commandRename->registerOption( 'no-color' ); 37 | 38 | return $commandRename; 39 | -------------------------------------------------------------------------------- /lib/commands/show.php: -------------------------------------------------------------------------------- 1 | 16 | * @alias track 17 | */ 18 | $commandShow = new Command( 'show', 'Show tracked projects', function( $name=null ) 19 | { 20 | $pug = new Pug(); 21 | 22 | /* Show Git metadata */ 23 | $showGit = $this->getOptionValue( 'A' ) == true; 24 | $showGit = $showGit || $this->getOptionValue( 'g' ) == true; 25 | $showGit = $showGit || $this->getOptionValue( 'git' ) == true; 26 | 27 | /* Show project path */ 28 | $showPath = $this->getOptionValue( 'A' ) == true; 29 | $showPath = $showPath || $this->getOptionValue( 'p' ) == true; 30 | $showPath = $showPath || $this->getOptionValue( 'path' ) == true; 31 | 32 | /* Use color */ 33 | $useColor = $this->getOptionValue( 'no-color' ) == null; 34 | 35 | if( !is_null( $name ) ) 36 | { 37 | try 38 | { 39 | $project = $pug->getProject( $name ); 40 | } 41 | catch( \Exception $e ) 42 | { 43 | throw new Command\CommandInvokedException( $e->getMessage(), 1 ); 44 | } 45 | 46 | $projects = [ $project ]; 47 | $showGit = true; 48 | $showPath = true; 49 | } 50 | else 51 | { 52 | $projects = $pug->getProjects( $this->getOptionValue( 't' ) ); 53 | } 54 | 55 | if( count( $projects ) < 1 ) 56 | { 57 | $output = new Output(); 58 | $output->line( 'pug: Not tracking any projects. See \'pug help\'' ); 59 | } 60 | else 61 | { 62 | $output = listProjects( $projects, $showGit, $showPath, $useColor ); 63 | } 64 | 65 | return $output->flush(); 66 | }); 67 | 68 | // Options 69 | $commandShow->registerOption( 'A' ); 70 | $commandShow->registerOption( 'g' ); 71 | $commandShow->registerOption( 'git' ); 72 | $commandShow->registerOption( 'no-color' ); 73 | $commandShow->registerOption( 'p' ); 74 | $commandShow->registerOption( 'path' ); 75 | $commandShow->registerOption( 't', 'Sort by time modified (most recently modified first) before sorting projects by name' ); 76 | 77 | // Aliases 78 | $commandShow->addAlias( 'list' ); 79 | $commandShow->addAlias( 'ls' ); 80 | 81 | // Usage 82 | $commandShowUsage = <<] 84 | 85 | OPTIONS 86 | -A, --all 87 | Include all project metadata in listing. 88 | 89 | -g, --git 90 | Include current branch and HEAD in listing. 91 | 92 | -p, --path 93 | Include project path in listing. 94 | 95 | -t 96 | Sort projects by time updated, most recent first. 97 | 98 | USAGE; 99 | 100 | $commandShow->setUsage( $commandShowUsage ); 101 | 102 | return $commandShow; 103 | -------------------------------------------------------------------------------- /lib/commands/update.php: -------------------------------------------------------------------------------- 1 | [--all]||] 15 | * @alias remove,untrack 16 | */ 17 | $commandUpdate = new CLI\Command('update', 'Fetch project updates', function( $query='./' ) 18 | { 19 | $pug = new Pug(); 20 | 21 | $options = $this->getOptionsWithValues(); 22 | $forceDependencyUpdate = isset( $options['f'] ) || isset( $options['force'] ); 23 | 24 | /* 25 | * Determine which projects we're going to update... 26 | * 27 | * The default behavior (with no arguments) is to attempt updating the working directory './' 28 | * 29 | * Note: Given the increased flexibility with Namespace support, 'pug update' no longer 30 | * supports multiple arguments. 31 | */ 32 | $projects = []; 33 | 34 | try 35 | { 36 | if( $query == 'all' ) 37 | { 38 | $projects = $pug->getEnabledProjects(); 39 | } 40 | elseif( $pug->namespaceExists( $query ) ) 41 | { 42 | $namespaceProjects = $pug->getProjectsInNamespace( $query ); 43 | 44 | /* Add all, including disabled */ 45 | if( $this->getOptionValue( 'all' ) ) 46 | { 47 | $projects = $namespaceProjects; 48 | } 49 | /* Add all enabled */ 50 | else 51 | { 52 | foreach( $namespaceProjects as $namespaceProject ) 53 | { 54 | if( $namespaceProject->isEnabled() ) 55 | { 56 | $projects[] = $namespaceProject; 57 | } 58 | } 59 | } 60 | } 61 | // ...or is this a project? 62 | else 63 | { 64 | $projects[] = $pug->getProject( $query ); 65 | } 66 | } 67 | catch( \Exception $e ) 68 | { 69 | throw new Command\CommandInvokedException( "No groups or projects match '{$query}'.", 1 ); 70 | } 71 | 72 | /* 73 | * Now update them 74 | */ 75 | for( $i=0; $i < count( $projects ); $i++ ) 76 | { 77 | $target = $projects[$i]; 78 | 79 | try 80 | { 81 | $pug->updateProject( $target, $forceDependencyUpdate ); 82 | } 83 | catch( \Exception $e ) 84 | { 85 | // Standard single-line failure with exit code 86 | if( count( $projects ) == 1 && $query != 'all' ) 87 | { 88 | throw new CLI\Command\CommandInvokedException( $e->getMessage(), 1 ); 89 | } 90 | 91 | $name = $target instanceof Project ? $target->getName() : $target; 92 | 93 | $stringHalted = new CLI\FormattedString( "Updating '{$name}'... halted:" ); 94 | $stringHalted->backgroundColor( 'red' ); 95 | 96 | $stringMessage = new CLI\FormattedString( " • {$e->getMessage()}" ); 97 | $stringMessage->foregroundColor( 'red' ); 98 | 99 | echo $stringHalted . PHP_EOL . PHP_EOL; 100 | echo $stringMessage . PHP_EOL . PHP_EOL; 101 | } 102 | } 103 | }); 104 | 105 | $commandUpdate->addAlias( 'up' ); 106 | $commandUpdate->registerOption( 'all', 'Update all projects in the group, even if disabled' ); 107 | $commandUpdate->registerOption( 'f', 'Force dependency managers to update' ); 108 | $commandUpdate->registerOption( 'force', 'Force dependency managers to update' ); 109 | 110 | $updateUsage = << [--all]||] 112 | 113 | OPTIONS 114 | --all 115 | update all projects in the group, even if disabled 116 | 117 | -f, --force 118 | force dependency managers (ex., CocoaPods) to update 119 | 120 | USAGE; 121 | 122 | $commandUpdate->setUsage( $updateUsage ); 123 | 124 | return $commandUpdate; 125 | -------------------------------------------------------------------------------- /lib/commands/upgrade.php: -------------------------------------------------------------------------------- 1 | getCurrentVersion(); 23 | 24 | $latestRelease = $pug->getLatestRelease(); 25 | $latestVersion = substr( $latestRelease['tag_name'], 1 ); 26 | 27 | $canUpgrade = version_compare( $currentVersion, $latestVersion, '<' ); 28 | 29 | if( !$canUpgrade ) 30 | { 31 | return "You're up-to-date! v{$currentVersion} is the latest version available."; 32 | }; 33 | 34 | /* 35 | * Upgrade 36 | */ 37 | echo PHP_EOL; 38 | echo " • Upgrading to {$latestRelease['tag_name']}... "; 39 | echo PHP_EOL . PHP_EOL; 40 | 41 | try 42 | { 43 | $pug->upgradeSelf(); 44 | } 45 | catch( \Exception $e ) 46 | { 47 | throw new Command\CommandInvokedException( "Could not upgrade: '{$e->getMessage()}'" ); 48 | } 49 | 50 | /* Display the release description */ 51 | $stringFormatted = new FormattedString(); 52 | 53 | $releaseBodyLines = explode( "\r\n", trim( $latestRelease['body'] ) ); 54 | foreach( $releaseBodyLines as $releaseBodyLine ) 55 | { 56 | $stringFormatted->foregroundColor( 'green' ); 57 | $stringFormatted->setString( " {$releaseBodyLine}" ); 58 | 59 | echo $stringFormatted . PHP_EOL; 60 | } 61 | echo PHP_EOL; 62 | }); 63 | 64 | return $commandUpgrade; 65 | -------------------------------------------------------------------------------- /lib/config.php: -------------------------------------------------------------------------------- 1 | 'pug', 8 | 9 | /* 10 | * Application version 11 | */ 12 | 'version' => '0.7.3', 13 | 14 | /* 15 | * PHP minimum version 16 | */ 17 | 'php-min' => '5.4' 18 | ); 19 | -------------------------------------------------------------------------------- /lib/pug/Autoloader.php: -------------------------------------------------------------------------------- 1 | projectPath = $projectPath; 34 | 35 | $podfile = new \SplFileInfo( $this->projectPath->getRealPath() . '/Podfile' ); 36 | 37 | if( $podfile->isFile() ) 38 | { 39 | $this->usesCocoaPods = true; 40 | $this->hashPodfileBefore = sha1_file( $podfile ); 41 | } 42 | } 43 | 44 | /** 45 | * @param boolean $force Force an update 46 | * @return boolean 47 | */ 48 | public function update( $force=false ) 49 | { 50 | if( $this->usesCocoaPods ) 51 | { 52 | echo PHP_EOL; 53 | echo ' • Updating CocoaPods... '; 54 | 55 | // a Pods folder exists 56 | $podsFolder = new \SplFileInfo( $this->projectPath->getRealPath() . '/Pods' ); 57 | $updateCocoaPods = $podsFolder->isDir() == false; 58 | 59 | // a lockfile exists 60 | $lockFile = new \SplFileInfo( $this->projectPath->getRealPath() . '/Podfile.lock' ); 61 | $updateCocoaPods = $updateCocoaPods || $lockFile->isFile() == false; 62 | 63 | // the Podfile was updated 64 | $podfile = new \SplFileInfo( $this->projectPath->getRealPath() . '/Podfile' ); 65 | $updateCocoaPods = $updateCocoaPods || $this->hashPodfileBefore != sha1_file( $podfile ); 66 | 67 | if( $updateCocoaPods || $force ) 68 | { 69 | Pug::executeCommand( 'pod install' ); 70 | } 71 | else 72 | { 73 | echo 'done.' . PHP_EOL; 74 | } 75 | 76 | return true; 77 | } 78 | 79 | return false; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /lib/pug/DependencyManager/Composer.php: -------------------------------------------------------------------------------- 1 | projectPath = $projectPath; 34 | 35 | $composerFile = new \SplFileInfo( $this->projectPath->getRealPath() . '/composer.json' ); 36 | 37 | if( $composerFile->isFile() ) 38 | { 39 | $this->usesComposer = true; 40 | $this->hashComposerFileBefore = sha1_file( $composerFile ); 41 | } 42 | } 43 | 44 | /** 45 | * @param boolean $force Force an update 46 | * @return boolean 47 | */ 48 | public function update( $force=false ) 49 | { 50 | if( $this->usesComposer ) 51 | { 52 | echo PHP_EOL; 53 | echo ' • Updating Composer... '; 54 | 55 | // a lockfile exists 56 | $lockFile = new \SplFileInfo( $this->projectPath->getRealPath() . '/composer.lock' ); 57 | $updateComposer = $lockFile->isFile() == false; 58 | 59 | // composer.json was updated 60 | $composerFile = new \SplFileInfo( $this->projectPath->getRealPath() . '/composer.json' ); 61 | $updateComposer = $updateComposer || $this->hashComposerFileBefore != sha1_file( $composerFile ); 62 | 63 | if( $updateComposer || $force ) 64 | { 65 | Pug::executeCommand( 'composer update' ); 66 | } 67 | else 68 | { 69 | echo 'done.' . PHP_EOL; 70 | } 71 | 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /lib/pug/DependencyManager/IDependencyManager.php: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /lib/pug/MissingSourceControlException.php: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /lib/pug/Project.php: -------------------------------------------------------------------------------- 1 | getRealPath() != false ) 63 | { 64 | $this->source = new File\Directory( $source->getRealPath() ); // expand relative paths 65 | } 66 | else 67 | { 68 | $this->source = $source; 69 | } 70 | 71 | /* 72 | * Parse for namespace 73 | */ 74 | if( substr_count( $name, self::NAMESPACE_DELIMITER ) > 0 ) 75 | { 76 | $namePieces = explode( self::NAMESPACE_DELIMITER, $name ); 77 | $this->namespace = self::getNormalizedNamespaceString( $namePieces[0] ); 78 | } 79 | 80 | $this->name = $name; 81 | 82 | $this->enabled = $enabled; 83 | $this->updated = $updated; 84 | } 85 | 86 | /** 87 | * @return void 88 | */ 89 | protected function detectSCM() 90 | { 91 | $this->scm = self::SCM_ERR; 92 | 93 | // Look for signs of SCM in working directory 94 | $dirCurrent = new File\Directory( $this->source->getRealPath() ); 95 | 96 | do 97 | { 98 | try 99 | { 100 | $dirGit = $dirCurrent->childDir( '.git' ); 101 | 102 | // Detecting a directory named .git instead of any matching file ensures that 103 | // we'll traverse up to and then update the project root instead of a submodule 104 | if( $dirGit->exists() ) 105 | { 106 | $this->scm = self::SCM_GIT; 107 | break; 108 | } 109 | } 110 | catch( \Exception $e ) 111 | { 112 | // .git exists but it isn't a directory. This probably means we're in a submodule... 113 | } 114 | 115 | $dirCurrent = $dirCurrent->parent(); 116 | } 117 | while( $dirCurrent->getPathname() != $dirCurrent->parent()->getPathname() ); 118 | } 119 | 120 | /** 121 | * @return void 122 | */ 123 | public function disable() 124 | { 125 | $this->enabled = false; 126 | } 127 | 128 | /** 129 | * @return void 130 | */ 131 | public function enable() 132 | { 133 | $this->enabled = true; 134 | } 135 | 136 | /** 137 | * @return boolean 138 | */ 139 | public function isEnabled() 140 | { 141 | return $this->enabled; 142 | } 143 | 144 | /** 145 | * @return string 146 | */ 147 | public function getActiveBranch() 148 | { 149 | $branchName = '-'; 150 | 151 | if( $this->source->exists() ) 152 | { 153 | chdir( $this->source ); 154 | $result = Pug::executeCommand( 'git rev-parse --abbrev-ref HEAD', false ); 155 | 156 | if( $result['exitCode'] === 0 ) 157 | { 158 | $branchName = $result['result']; 159 | } 160 | } 161 | 162 | return $branchName; 163 | } 164 | 165 | /** 166 | * @return string 167 | */ 168 | public function getCommitHash() 169 | { 170 | $commitHash = '-'; 171 | 172 | if( $this->source->exists() ) 173 | { 174 | chdir( $this->source ); 175 | $result = Pug::executeCommand( 'git log --pretty=format:"%h" -n 1', false ); 176 | 177 | if( $result['exitCode'] === 0 ) 178 | { 179 | $commitHash = $result['result']; 180 | } 181 | } 182 | 183 | return $commitHash; 184 | } 185 | 186 | /** 187 | * @param string $name 188 | * @return boolean|string 189 | */ 190 | public function getConfigValue( $name ) 191 | { 192 | $commandConfig = Pug::executeCommand( "git config {$name}", false ); 193 | 194 | switch( strtolower( $commandConfig['result'] ) ) 195 | { 196 | case '': 197 | $value = null; 198 | break; 199 | 200 | case 'false': 201 | $value = false; 202 | break; 203 | 204 | case 'true': 205 | $value = true; 206 | break; 207 | 208 | default: 209 | $value = $commandConfig['result']; 210 | break; 211 | } 212 | 213 | return $value; 214 | } 215 | 216 | /** 217 | * @return \SplFileInfo 218 | */ 219 | public function getFileInfo() 220 | { 221 | return $this->source; 222 | } 223 | 224 | /** 225 | * @return string 226 | */ 227 | public function getName() 228 | { 229 | return $this->name; 230 | } 231 | 232 | /** 233 | * @return string|null 234 | */ 235 | public function getNamespace() 236 | { 237 | return $this->namespace; 238 | } 239 | 240 | /** 241 | * @param string $namespace 242 | * @return string 243 | */ 244 | static public function getNormalizedNamespaceString( $namespace ) 245 | { 246 | // Strip trailing '/' 247 | if( substr( $namespace, -1 ) == Project::NAMESPACE_DELIMITER ) 248 | { 249 | $namespace = substr( $namespace, 0, strlen( $namespace ) - 1 ); 250 | } 251 | 252 | return $namespace; 253 | } 254 | 255 | /** 256 | * @return string 257 | */ 258 | public function getPath() 259 | { 260 | return $this->source->getPathname(); 261 | } 262 | 263 | /** 264 | * @return int 265 | */ 266 | public function getSCM() 267 | { 268 | if( is_null( $this->scm ) ) 269 | { 270 | $this->detectSCM(); 271 | } 272 | 273 | return $this->scm; 274 | } 275 | 276 | /** 277 | * Take inventory of all submodule states 278 | * 279 | * @return array 280 | */ 281 | public function getSubmoduleInventory() 282 | { 283 | $inventory = []; 284 | 285 | $delimiter = '{PUG_DELIMITER}'; 286 | $commandSubmoduleStatus = "git submodule foreach --quiet --recursive 'echo \$name {$delimiter} \$toplevel/\$path {$delimiter} \$sha1 {$delimiter} `git rev-parse --abbrev-ref HEAD`'"; 287 | $resultSubmoduleStatus = Pug::executeCommand( $commandSubmoduleStatus, false ); 288 | 289 | foreach( $resultSubmoduleStatus['output'] as $result ) 290 | { 291 | $resultPieces = explode( $delimiter, $result ); 292 | 293 | $submoduleName = trim( $resultPieces[0] ); 294 | $dirSubmodule = new File\Directory( trim( $resultPieces[1] ) ); 295 | $submoduleCommit = trim( $resultPieces[2] ); 296 | $submoduleBranch = trim( $resultPieces[3] ); 297 | 298 | $projectSubmodule = new Project( $submoduleName, $dirSubmodule ); 299 | $inventory[$submoduleName] = 300 | [ 301 | 'project' => $projectSubmodule, 302 | 'commit' => $submoduleCommit, 303 | 'branch' => $submoduleBranch, 304 | ]; 305 | } 306 | 307 | return $inventory; 308 | } 309 | /** 310 | * Returns a timestamp of the project's last update 311 | * 312 | * @return string 313 | */ 314 | public function getUpdated() 315 | { 316 | return $this->updated; 317 | } 318 | 319 | /** 320 | * @param string $name 321 | * @return void 322 | */ 323 | public function setName( $name ) 324 | { 325 | $this->name = $name; 326 | } 327 | 328 | /** 329 | * Update a project's working copy and its dependencies 330 | * 331 | * @param boolean $forceDependencyUpdate 332 | * @return void 333 | */ 334 | public function update( $forceDependencyUpdate ) 335 | { 336 | $this->detectSCM(); 337 | 338 | if( !$this->getFileInfo()->isDir() ) 339 | { 340 | throw new InvalidDirectoryException( "Project root '{$this->source}' is not a valid directory." ); 341 | } 342 | if( !$this->getFileInfo()->isReadable() ) 343 | { 344 | throw new InvalidDirectoryException( "Project root '{$this->source}' isn't readable." ); 345 | } 346 | if( $this->scm == self::SCM_ERR ) 347 | { 348 | throw new MissingSourceControlException( "Source control not found in '{$this->source->getPathname()}'." ); 349 | } 350 | 351 | chdir( $this->source->getPathname() ); 352 | 353 | echo "Updating '{$this->getName()}'... " . PHP_EOL . PHP_EOL; 354 | 355 | // Set up dependency managers 356 | $cocoaPods = new DependencyManager\CocoaPods( $this->source ); 357 | $composer = new DependencyManager\Composer( $this->source ); 358 | 359 | // Update the main repository 360 | $stashChanges = $this->getConfigValue( 'pug.update.stash' ) == true; 361 | 362 | if( $stashChanges ) 363 | { 364 | echo ' • Stashing local changes... '; 365 | $resultStashed = Pug::executeCommand( 'git stash save "pug: automatically stashing changes"' ); 366 | echo PHP_EOL; 367 | } 368 | 369 | // Before we update anything, take a snapshot of submodule states 370 | $submoduleInventory = $this->getSubmoduleInventory(); 371 | 372 | /* 373 | * Get updates 374 | */ 375 | /* Fetch & Rebase */ 376 | if( $this->getConfigValue( 'pug.update.rebase' ) == true ) 377 | { 378 | echo ' • Fetching... '; 379 | $resultGitFetch = Pug::executeCommand( 'git fetch' ); 380 | 381 | echo PHP_EOL; 382 | echo ' • Rebasing... '; 383 | $resultGitRebase = Pug::executeCommand( 'git rebase' ); 384 | } 385 | /* Pull (Fetch & Merge) */ 386 | else 387 | { 388 | echo ' • Pulling... '; 389 | Pug::executeCommand( 'git pull' ); 390 | } 391 | 392 | /* Pop stash */ 393 | if( $stashChanges && $resultStashed['result'] != 'No local changes to save' ) 394 | { 395 | echo PHP_EOL; 396 | echo ' • Popping stash... '; 397 | Pug::executeCommand( 'git stash pop' ); 398 | } 399 | 400 | /* 401 | * Submodules 402 | */ 403 | $this->updateSubmodules( $submoduleInventory ); 404 | 405 | /* 406 | * Update dependencies if necessary 407 | */ 408 | $cocoaPods->update( $forceDependencyUpdate ); 409 | $composer->update( $forceDependencyUpdate ); 410 | 411 | echo PHP_EOL; 412 | 413 | $this->updated = time(); 414 | } 415 | 416 | /** 417 | * @param array $preInventory 418 | * @return void 419 | */ 420 | public function updateSubmodules( array $preInventory ) 421 | { 422 | $modulesFile = $this->source->child( '.gitmodules' ); 423 | 424 | if( !$modulesFile->exists() ) 425 | { 426 | return; 427 | } 428 | 429 | $updateSubmodules = $this->getConfigValue( 'pug.update.submodules' ) !== false; 430 | if( !$updateSubmodules ) 431 | { 432 | echo PHP_EOL; 433 | echo ' • Submodule updates were skipped due to configuration'; 434 | echo PHP_EOL; 435 | 436 | return; 437 | } 438 | 439 | echo PHP_EOL; 440 | 441 | /* 442 | * Perform the actual update 443 | */ 444 | echo ' • Updating submodules... '; 445 | Pug::executeCommand( 'git submodule update --init --recursive' ); 446 | 447 | // Now that we've updated everything, take another snapshot of submodule states 448 | $postInventory = $this->getSubmoduleInventory(); 449 | 450 | /* 451 | * Restore submodules to previous states as appropriate 452 | */ 453 | foreach( $postInventory as $submoduleName => $postUpdateInfo ) 454 | { 455 | $projectSubmodule = $postUpdateInfo['project']; 456 | $pathSubmodule = $projectSubmodule->getPath(); 457 | 458 | $stringFormatted = new FormattedString(); 459 | $stringFormatted->foregroundColor( 'blue' ); 460 | 461 | if( !isset( $preInventory[$submoduleName] ) ) 462 | { 463 | continue; 464 | } 465 | 466 | $preUpdateInfo = $preInventory[$submoduleName]; 467 | 468 | /* 469 | * Submodules that were checked out to a branch before the update 470 | */ 471 | if( $preUpdateInfo['branch'] != 'HEAD' ) 472 | { 473 | /* 474 | * We're in a detached state now 475 | */ 476 | if( $preUpdateInfo['branch'] != $postUpdateInfo['branch'] ) 477 | { 478 | chdir( $pathSubmodule ); 479 | 480 | // Check the submodule out the the previous branch 481 | Pug::executeCommand( "git checkout {$preUpdateInfo['branch']}", false ); 482 | 483 | $messageCheckout = " % Submodule path '{$projectSubmodule->getName()}': checked out '{$preUpdateInfo['branch']}'"; 484 | $stringFormatted->setString( $messageCheckout ); 485 | echo $stringFormatted . PHP_EOL; 486 | } 487 | 488 | // If the pointer has changed, we should pull down submodule changes as well...? 489 | if( $preUpdateInfo['commit'] != $postUpdateInfo['commit'] ) 490 | { 491 | chdir( $pathSubmodule ); 492 | 493 | $messagePulling = " % Submodule path '{$projectSubmodule->getName()}': pulling '{$preUpdateInfo['branch']}'... "; 494 | $stringFormatted->setString( $messagePulling ); 495 | echo $stringFormatted; 496 | 497 | Pug::executeCommand( 'git pull', false ); 498 | 499 | $stringFormatted->setString( 'done.' ); 500 | echo $stringFormatted . PHP_EOL; 501 | } 502 | } 503 | } 504 | } 505 | 506 | /** 507 | * @return array 508 | */ 509 | public function jsonSerialize() 510 | { 511 | return [ 512 | 'name' => $this->getName(), 513 | 'path' => $this->source->getPathname(), 514 | 'enabled' => $this->enabled, 515 | 'updated' => $this->updated 516 | ]; 517 | } 518 | } 519 | -------------------------------------------------------------------------------- /lib/pug/Pug.php: -------------------------------------------------------------------------------- 1 | pugFile = new File\File( getenv('HOME') . '/.pug' ); 42 | } 43 | else 44 | { 45 | $this->pugFile = new File\File( $envPugFile ); 46 | } 47 | 48 | /* 49 | * Make sure the config file is ready to go 50 | */ 51 | if( !$this->pugFile->exists() ) 52 | { 53 | $this->pugFile->create(); 54 | } 55 | 56 | if( !$this->pugFile->isReadable() ) 57 | { 58 | throw new \Exception( 'Can\'t read from ' . $this->pugFile, 1 ); 59 | } 60 | if( !$this->pugFile->isWritable() ) 61 | { 62 | throw new \Exception( 'Can\'t write to ' . $this->pugFile, 1 ); 63 | } 64 | 65 | $json = json_decode( $this->pugFile->getContents(), true ); 66 | 67 | /* 68 | * Load projects 69 | */ 70 | if( isset( $json['projects'] ) ) 71 | { 72 | foreach( $json['projects'] as $projectInfo ) 73 | { 74 | $enabled = isset( $projectInfo['enabled'] ) ? $projectInfo['enabled'] : true; 75 | $updated = isset( $projectInfo['updated'] ) ? $projectInfo['updated'] : null; 76 | 77 | try 78 | { 79 | $dirProject = new File\Directory( $projectInfo['path'] ); 80 | } 81 | catch( \Exception $e ) 82 | { 83 | continue; 84 | } 85 | 86 | $project = new Project( $projectInfo['name'], $dirProject, $enabled, $updated ); 87 | 88 | $this->projects[] = $project; 89 | 90 | /* Namespace */ 91 | $projectNamespace = $project->getNamespace(); 92 | if( !is_null( $projectNamespace ) && !in_array( $projectNamespace, $this->namespaces ) ) 93 | { 94 | $this->namespaces[] = $projectNamespace; 95 | } 96 | } 97 | } 98 | 99 | /* 100 | * Define Pug directory 101 | */ 102 | $pathPug = dirname( dirname( __DIR__ ) ); 103 | $this->dirPug = new File\Directory( $pathPug ); 104 | 105 | $this->sortProjects(); 106 | } 107 | 108 | /** 109 | * @param Project $project 110 | */ 111 | public function addProject(Project $project) 112 | { 113 | foreach($this->projects as $current) 114 | { 115 | if($project->getName() == $current->getName()) 116 | { 117 | throw new \Exception("Project '{$project->getName()}' already exists. See 'pug show'.", 1); 118 | } 119 | } 120 | 121 | if( $project->getSCM() == Project::SCM_ERR ) 122 | { 123 | throw new \Exception( "Source control not found in '{$project->getPath()}'." ); 124 | } 125 | 126 | $this->projects[] = $project; 127 | $this->write(); 128 | } 129 | 130 | /** 131 | * @return void 132 | */ 133 | public function disableAllProjects() 134 | { 135 | foreach( $this->projects as &$project ) 136 | { 137 | $project->disable(); 138 | } 139 | 140 | $this->write(); 141 | } 142 | 143 | /** 144 | * @param string $name 145 | * @return void 146 | */ 147 | public function disableProject( $name ) 148 | { 149 | $count = count($this->projects); 150 | $disabled = 0; 151 | 152 | for( $i=0; $i<$count; $i++ ) 153 | { 154 | if( $this->projects[$i]->getName() == $name ) 155 | { 156 | $this->projects[$i]->disable(); 157 | $disabled++; 158 | } 159 | } 160 | 161 | if( $disabled == 0 ) 162 | { 163 | throw new CLI\Command\CommandInvokedException("Project '{$name}' not found.", 1); 164 | } 165 | 166 | $this->write(); 167 | } 168 | 169 | /** 170 | * @param string $namespace 171 | * @return void 172 | */ 173 | public function disableProjectsInNamespace( $namespace ) 174 | { 175 | $namespace = Project::getNormalizedNamespaceString( $namespace ); 176 | 177 | $projects = $this->getProjectsInNamespace( $namespace ); 178 | 179 | foreach( $projects as &$project ) 180 | { 181 | if( $project->getNamespace() == $namespace ) 182 | { 183 | $project->disable(); 184 | } 185 | } 186 | 187 | $this->write(); 188 | } 189 | 190 | /** 191 | * @return void 192 | */ 193 | public function enableAllProjects() 194 | { 195 | foreach( $this->projects as &$project ) 196 | { 197 | $project->enable(); 198 | } 199 | 200 | $this->write(); 201 | } 202 | 203 | /** 204 | * @param string $name 205 | * @return void 206 | */ 207 | public function enableProject( $name ) 208 | { 209 | $count = count($this->projects); 210 | $enabled = 0; 211 | 212 | for( $i=0; $i<$count; $i++ ) 213 | { 214 | if( $this->projects[$i]->getName() == $name ) 215 | { 216 | $this->projects[$i]->enable(); 217 | $enabled++; 218 | } 219 | } 220 | 221 | if($enabled == 0) 222 | { 223 | throw new CLI\Command\CommandInvokedException("Project '{$name}' not found.", 1); 224 | } 225 | 226 | $this->write(); 227 | } 228 | 229 | /** 230 | * @param string $namespace 231 | * @return void 232 | */ 233 | public function enableProjectsInNamespace( $namespace ) 234 | { 235 | $namespace = Project::getNormalizedNamespaceString( $namespace ); 236 | 237 | $projects = $this->getProjectsInNamespace( $namespace ); 238 | 239 | foreach( $projects as &$project ) 240 | { 241 | if( $project->getNamespace() == $namespace ) 242 | { 243 | $project->enable(); 244 | } 245 | } 246 | 247 | $this->write(); 248 | } 249 | 250 | /** 251 | * Execute a command, generate friendly output and return the result 252 | * 253 | * @param string $command 254 | * @param boolean $echo 255 | * @return boolean 256 | */ 257 | static public function executeCommand( $command, $echo=true ) 258 | { 259 | $command = $command . ' 2>&1'; // force output to be where we need it 260 | $result = exec( $command, $outputCommand, $exitCode ); 261 | $output = []; 262 | 263 | if( count( $outputCommand ) == 0 ) 264 | { 265 | $output[] = 'done.'; 266 | } 267 | else 268 | { 269 | $output[] = ''; 270 | $color = $exitCode == 0 ? 'green' : 'red'; 271 | 272 | foreach( $outputCommand as $line ) 273 | { 274 | $formattedLine = new CLI\FormattedString( " > {$line}" ); 275 | $formattedLine->foregroundColor( $color ); 276 | 277 | if( strlen( $line ) > 0 ) 278 | { 279 | $output[] = $formattedLine; 280 | } 281 | } 282 | } 283 | 284 | if( $echo ) 285 | { 286 | foreach( $output as $line ) 287 | { 288 | echo $line . PHP_EOL; 289 | } 290 | } 291 | 292 | return [ 293 | 'output' => $outputCommand, 294 | 'result' => $result, 295 | 'exitCode' => $exitCode 296 | ]; 297 | } 298 | 299 | /** 300 | * @return string 301 | */ 302 | public function getCurrentVersion() 303 | { 304 | $fileConfig = $this->dirPug 305 | ->childDir( 'lib' ) 306 | ->child( 'config.php' ); 307 | 308 | $config = include( $fileConfig ); 309 | 310 | return $config['version']; 311 | } 312 | 313 | /** 314 | * Return array of all enabled projects 315 | * 316 | * @return array 317 | */ 318 | public function getEnabledProjects() 319 | { 320 | $enabled = []; 321 | 322 | foreach( $this->projects as $project ) 323 | { 324 | if( $project->isEnabled() ) 325 | { 326 | $enabled[] = $project; 327 | } 328 | } 329 | 330 | return $enabled; 331 | } 332 | 333 | /** 334 | * @return array 335 | */ 336 | public function getLatestRelease() 337 | { 338 | $urlRepoReleases = 'https://api.github.com/repos/ashur/pug/releases'; 339 | 340 | /* Header */ 341 | $httpRequest = new HTTP\Request( $urlRepoReleases ); 342 | $httpRequest->addHeader( 'User-Agent', 'ashur/pug' ); 343 | 344 | /* Perform request */ 345 | $httpResponse = HTTP::get( $httpRequest ); 346 | $responseStatus = $httpResponse->getStatus(); 347 | 348 | if( $responseStatus['code'] >= 400 ) 349 | { 350 | throw new \Exception( "GitHub returned an error: '{$responseStatus['message']}'" ); 351 | } 352 | 353 | $releases = json_decode( $httpResponse->getBody(), true ); 354 | if( json_last_error() != JSON_ERROR_NONE ) 355 | { 356 | $jsonError = json_last_error_msg(); 357 | throw new \Exception( "Couldn't understand the response from GitHub: '{$jsonError}'" ); 358 | } 359 | 360 | if( !is_array( $releases ) ) 361 | { 362 | throw new \Exception( "GitHub returned an unexpected response" ); 363 | } 364 | 365 | $latestRelease = array_shift( $releases ); 366 | return $latestRelease; 367 | } 368 | 369 | /** 370 | * @param string $name 371 | * @return Project 372 | */ 373 | public function getProject( $name ) 374 | { 375 | foreach( $this->projects as &$project ) 376 | { 377 | if($project->getName() == $name) 378 | { 379 | return $project; 380 | } 381 | } 382 | 383 | // No registered project matches, let's try a file path 384 | $dirProject = new File\Directory( $name ); 385 | 386 | if( $dirProject->exists() ) 387 | { 388 | $projectPath = $dirProject->getRealpath(); 389 | 390 | // Let's check to see if a tracked project is already registered at this path 391 | foreach( $this->projects as &$project ) 392 | { 393 | if( strtolower( $project->getPath() ) == strtolower( $projectPath ) ) 394 | { 395 | return $project; 396 | } 397 | } 398 | 399 | // Definitely no registered project matches, down to the bare file path itself 400 | return new Project( $dirProject->getRealpath(), $dirProject ); 401 | } 402 | 403 | // No project or file path matches, time to bail 404 | throw new \Exception( "Unknown project or directory '{$name}'." ); 405 | } 406 | 407 | /** 408 | * @param boolean $sortByUpdated 409 | * @return array 410 | */ 411 | public function getProjects( $sortByUpdated = false ) 412 | { 413 | if( $sortByUpdated ) 414 | { 415 | $this->sortProjects( $sortByUpdated ); 416 | } 417 | 418 | return $this->projects; 419 | } 420 | 421 | /** 422 | * @param string $namespace 423 | * @return array 424 | */ 425 | public function getProjectsInNamespace( $namespace ) 426 | { 427 | $namespace = Project::getNormalizedNamespaceString( $namespace ); 428 | 429 | if( !$this->namespaceExists( $namespace ) ) 430 | { 431 | throw new \Exception( "Namespace '{$namespace}' not found." ); 432 | } 433 | 434 | $matches = []; 435 | 436 | foreach( $this->projects as $project ) 437 | { 438 | if( $project->getNamespace() == $namespace ) 439 | { 440 | $matches[] = $project; 441 | } 442 | } 443 | 444 | return $matches; 445 | } 446 | 447 | /** 448 | * @param string $namespace 449 | * @return boolean 450 | */ 451 | public function namespaceExists( $namespace ) 452 | { 453 | $namespace = Project::getNormalizedNamespaceString( $namespace ); 454 | 455 | return in_array( $namespace, $this->namespaces ); 456 | } 457 | 458 | /** 459 | * @return void 460 | */ 461 | public function removeAllProjects() 462 | { 463 | $this->projects = []; 464 | 465 | $this->write(); 466 | } 467 | 468 | /** 469 | * @param string $name 470 | */ 471 | public function removeProject($name) 472 | { 473 | $count = count($this->projects); 474 | $removed = 0; 475 | 476 | for($i=0; $i < $count; $i++) 477 | { 478 | if($this->projects[$i]->getName() == $name) 479 | { 480 | unset($this->projects[$i]); 481 | $removed++; 482 | } 483 | } 484 | 485 | if( $removed == 0 ) 486 | { 487 | throw new CLI\Command\CommandInvokedException("Project '{$name}' not found.", 1); 488 | } 489 | 490 | $this->write(); 491 | } 492 | 493 | /** 494 | * @param string $namespace 495 | * @return void 496 | */ 497 | public function removeProjectsInNamespace( $namespace ) 498 | { 499 | $projects = $this->getProjectsInNamespace( $namespace ); 500 | 501 | foreach( $projects as &$project ) 502 | { 503 | if( $project->getNamespace() == $namespace ) 504 | { 505 | $this->removeProject( $project->getName() ); 506 | } 507 | } 508 | 509 | $this->write(); 510 | } 511 | 512 | /** 513 | * @param string $oldName 514 | * @param string $newName 515 | * @return void 516 | */ 517 | public function renameProject( $oldName, $newName ) 518 | { 519 | // Does another project already have the desired name? 520 | try 521 | { 522 | $project = $this->getProject( $oldName ); 523 | } 524 | catch( \Exception $e ) 525 | { 526 | throw new \Exception( "No project matches '{$oldName}'." ); 527 | } 528 | 529 | // Does another project already have the desired name? 530 | try 531 | { 532 | $this->getProject( $newName ); 533 | } 534 | catch( \Exception $e ) 535 | { 536 | // Counterintuitively, an exception means there was no match so we can proceed. 537 | $project->setName( $newName ); 538 | $this->write(); 539 | 540 | return; 541 | } 542 | 543 | throw new \Exception( "A project named '{$newName}' already exists." ); 544 | } 545 | 546 | /** 547 | * @param Project $project 548 | */ 549 | public function setPathForProject(Project $project) 550 | { 551 | $updated = 0; 552 | for($i=0; $i < count($this->projects); $i++) 553 | { 554 | if($this->projects[$i]->getName() == $project->getName()) 555 | { 556 | $this->projects[$i] = $project; 557 | $updated++; 558 | } 559 | } 560 | 561 | if($updated == 0) 562 | { 563 | throw new CLI\Command\CommandInvokedException("Project '{$project->getName()}' not found.", 1); 564 | } 565 | 566 | $this->write(); 567 | } 568 | 569 | /** 570 | * @param boolean $sortByUpdated 571 | */ 572 | protected function sortProjects($sortByUpdated = false) 573 | { 574 | $name = []; 575 | $updated = []; 576 | 577 | // Sort projects by name 578 | foreach($this->projects as $project) 579 | { 580 | $name[] = $project->getName(); 581 | $updated[] = $project->getUpdated(); 582 | } 583 | 584 | if ($sortByUpdated == true) 585 | { 586 | array_multisort($updated, SORT_DESC, $name, SORT_ASC, $this->projects); 587 | return; 588 | } 589 | 590 | array_multisort($name, SORT_ASC, $this->projects); 591 | } 592 | 593 | /** 594 | * Attempt to update a single project based on its target (registered project name or filepath) 595 | * 596 | * @param string $target Target to update 597 | * @param boolean $forceDependencyUpdate 598 | */ 599 | public function updateProject( $target, $forceDependencyUpdate=false ) 600 | { 601 | if( $target instanceof Project ) 602 | { 603 | $project = $target; 604 | } 605 | else 606 | { 607 | $project = $this->getProject( $target ); 608 | } 609 | 610 | $project->update( $forceDependencyUpdate ); 611 | $this->write(); 612 | } 613 | 614 | /** 615 | * Run 'git pull' and 'git submodule update...' on the local pug repo itself 616 | */ 617 | public function upgradeSelf() 618 | { 619 | chdir( $this->dirPug->getPathname() ); 620 | 621 | /* Pull */ 622 | $resultPull = self::executeCommand( 'git pull', false ); 623 | if( $resultPull['exitCode'] != 0 ) 624 | { 625 | throw new \Exception( array_shift( $resultPull['output'] ) ); 626 | } 627 | 628 | /* Update submodules */ 629 | $resultUpdateSubmodules = self::executeCommand( 'git submodule update --init --recursive', false ); 630 | if( $resultUpdateSubmodules['exitCode'] != 0 ) 631 | { 632 | throw new \Exception( array_shift( $resultUpdateSubmodules['output'] ) ); 633 | } 634 | } 635 | 636 | /** 637 | */ 638 | protected function write() 639 | { 640 | $this->sortProjects(); 641 | $projects = $this->projects; 642 | 643 | $json = json_encode(compact('projects'), JSON_PRETTY_PRINT); 644 | 645 | file_put_contents($this->pugFile, $json); 646 | } 647 | } 648 | -------------------------------------------------------------------------------- /pug: -------------------------------------------------------------------------------- 1 | ./bin/pug -------------------------------------------------------------------------------- /test/commands/PugTestCase.php: -------------------------------------------------------------------------------- 1 | assertEquals( file_get_contents( $sourceFilename ), file_get_contents( $targetFilename ) ); 85 | } 86 | else 87 | { 88 | throw new \Exception( "Unknown pugfile '{$filename}'" ); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /test/commands/addTest.php: -------------------------------------------------------------------------------- 1 | executePugCommand( 'add' ); 24 | 25 | $this->assertEquals( 1, $result['exit'] ); 26 | 27 | $expectedOutput = <</] [--url=] 29 | 30 | OPTIONS 31 | --url= 32 | Clone the Git repository at to . 33 | 34 | OUTPUT; 35 | 36 | $this->assertEquals( $expectedOutput, $result['output'] ); 37 | } 38 | 39 | public function testAddNonExistentPathReturnsError() 40 | { 41 | $path = self::$fixturesPath . '/' . microtime( true ); 42 | $result = $this->executePugCommand( 'add', ['pug/1', $path] ); 43 | 44 | $this->assertEquals( 1, $result['exit'] ); 45 | $this->assertEquals( "pug: Couldn't track project. Path '{$path}' not found.", $result['output'] ); 46 | } 47 | 48 | public function testAddExistingFileReturnsError() 49 | { 50 | $this->usePugfile( '.pug-new' ); 51 | 52 | $path = self::$pugfilePath; 53 | $result = $this->executePugCommand( 'add', ['pug/1', $path] ); 54 | 55 | $this->assertEquals( 1, $result['exit'] ); 56 | $this->assertEquals( "pug: Couldn't track project. Invalid directory '{$path}'", $result['output'] ); 57 | } 58 | 59 | public function testAddExistingNameReturnsError() 60 | { 61 | $this->usePugfile( '.pug-enabled' ); 62 | 63 | $projectName = 'pug/1'; 64 | $result = $this->executePugCommand( 'add', [$projectName, self::$projectPath] ); 65 | 66 | $this->assertEquals( 1, $result['exit'] ); 67 | $this->assertEquals( "pug: Couldn't track project. Project '{$projectName}' already exists. See 'pug show'.", $result['output'] ); 68 | } 69 | 70 | public function testAddDirectoryWithoutSCMReturnsError() 71 | { 72 | $projectName = 'pug/3'; 73 | $projectPath = dirname( self::$projectPath ); // Parent folder of repo folder should not be under SCM 74 | 75 | $result = $this->executePugCommand( 'add', [$projectName, $projectPath] ); 76 | 77 | $this->assertEquals( 1, $result['exit'] ); 78 | $this->assertEquals( "pug: Couldn't track project. Source control not found in '{$projectPath}'.", $result['output'] ); 79 | } 80 | 81 | /** 82 | * @dataProvider projectNameProvider 83 | */ 84 | public function testAddProjectReturnsListing( $projectName ) 85 | { 86 | $this->usePugfile( '.pug-disabled' ); 87 | 88 | $result = $this->executePugCommand( 'add', [$projectName, self::$fixturesPath] ); 89 | 90 | $this->assertEquals( 0, $result['exit'] ); 91 | 92 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 99 | } 100 | 101 | public function testAddWhoseNameCollidesWithExistingGroupReturnsError() 102 | { 103 | $this->markTestIncomplete(); 104 | 105 | $this->usePugfile( '.pug-enabled' ); 106 | 107 | $projectName = 'pug'; 108 | $projectPath = self::$projectPath; 109 | 110 | $result = $this->executePugCommand( 'add', [$projectName, $projectPath] ); 111 | 112 | $this->assertEquals( 1, $result['exit'] ); 113 | $this->assertEquals( "pug: Couldn't track project. Group '{$projectName}' already exists. See 'pug show'.", $result['output'] ); 114 | } 115 | } 116 | -------------------------------------------------------------------------------- /test/commands/disableTest.php: -------------------------------------------------------------------------------- 1 | usePugfile( '.pug-enabled' ); 16 | $result = $this->executePugCommand( 'disable', ['all'] ); 17 | 18 | $this->assertEquals( 0, $result['exit'] ); 19 | 20 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 26 | } 27 | 28 | public function testDisableEmptyQueryDisplaysUsage() 29 | { 30 | $result = $this->executePugCommand( 'disable' ); 31 | 32 | $this->assertEquals( 1, $result['exit'] ); 33 | $this->assertEquals( 'usage: pug disable [all||]', $result['output'] ); 34 | } 35 | 36 | public function testDisableExistingGroup() 37 | { 38 | $this->usePugfile( '.pug-enabled' ); 39 | $result = $this->executePugCommand( 'disable', ['pug'] ); 40 | 41 | $this->assertEquals( 0, $result['exit'] ); 42 | 43 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 49 | } 50 | 51 | public function testDisableExistingSingleProject() 52 | { 53 | $this->usePugfile( '.pug-enabled' ); 54 | $result = $this->executePugCommand( 'disable', ['pug/1'] ); 55 | 56 | $this->assertEquals( 0, $result['exit'] ); 57 | 58 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 64 | } 65 | 66 | public function testDisableNonExistentTargetReturnsError() 67 | { 68 | $targetName = microtime( true ); 69 | $result = $this->executePugCommand( 'disable', [$targetName] ); 70 | 71 | $this->assertEquals( 1, $result['exit'] ); 72 | $this->assertEquals( "pug: No groups or projects match '{$targetName}'.", $result['output'] ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/commands/enableTest.php: -------------------------------------------------------------------------------- 1 | usePugfile( '.pug-disabled' ); 16 | $result = $this->executePugCommand( 'enable', ['all'] ); 17 | 18 | $this->assertEquals( 0, $result['exit'] ); 19 | 20 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 26 | } 27 | 28 | public function testEnableEmptyQueryDisplaysUsage() 29 | { 30 | $result = $this->executePugCommand( 'enable' ); 31 | 32 | $this->assertEquals( 1, $result['exit'] ); 33 | $this->assertEquals( 'usage: pug enable [all||]', $result['output'] ); 34 | } 35 | 36 | public function testEnableExistingGroup() 37 | { 38 | $this->usePugfile( '.pug-disabled' ); 39 | $result = $this->executePugCommand( 'enable', ['pug'] ); 40 | 41 | $this->assertEquals( 0, $result['exit'] ); 42 | 43 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 49 | } 50 | 51 | public function testEnableExistingSingleProject() 52 | { 53 | $this->usePugfile( '.pug-disabled' ); 54 | $result = $this->executePugCommand( 'enable', ['pug/1'] ); 55 | 56 | $this->assertEquals( 0, $result['exit'] ); 57 | 58 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 64 | } 65 | 66 | public function testEnableNonExistentTargetReturnsError() 67 | { 68 | $targetName = microtime( true ); 69 | $result = $this->executePugCommand( 'enable', [$targetName] ); 70 | 71 | $this->assertEquals( 1, $result['exit'] ); 72 | $this->assertEquals( "pug: No groups or projects match '{$targetName}'.", $result['output'] ); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /test/commands/removeTest.php: -------------------------------------------------------------------------------- 1 | executePugCommand( 'remove' ); 16 | 17 | $this->assertEquals( 1, $result['exit'] ); 18 | 19 | $expectedOutput = <<[/]|] [-y|--yes|--assume-yes] 21 | 22 | OPTIONS 23 | -y, --yes, --assume-yes 24 | Automatic yes to prompts. Assume "yes" as answer to all prompts and run 25 | non-interactively. 26 | 27 | OUTPUT; 28 | 29 | $this->assertEquals( $expectedOutput, $result['output'] ); 30 | } 31 | 32 | public function testRemoveAllReturnsNoOutput() 33 | { 34 | $this->usePugfile( '.pug-enabled' ); 35 | $result = $this->executePugCommand( 'remove', ['all', '--yes'] ); 36 | 37 | $this->assertEquals( 0, $result['exit'] ); 38 | $this->assertEquals( '', $result['output'] ); 39 | } 40 | 41 | public function testRemoveGroupReturnsListing() 42 | { 43 | $this->usePugfile( '.pug-groups' ); 44 | $result = $this->executePugCommand( 'remove', ['green', '--yes'] ); 45 | 46 | $this->assertEquals( 0, $result['exit'] ); 47 | 48 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 54 | } 55 | 56 | public function testRemoveProjectReturnsListing() 57 | { 58 | $this->usePugfile( '.pug-groups' ); 59 | $result = $this->executePugCommand( 'remove', ['red/1'] ); 60 | 61 | $this->assertEquals( 0, $result['exit'] ); 62 | 63 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 69 | } 70 | 71 | public function testRemoveNonExistentTargetReturnsError() 72 | { 73 | $this->usePugfile( '.pug-groups' ); 74 | 75 | $targetName = microtime( true ); 76 | $result = $this->executePugCommand( 'remove', [$targetName] ); 77 | 78 | $this->assertEquals( 1, $result['exit'] ); 79 | $this->assertEquals( "pug: No groups or projects match '{$targetName}'.", $result['output'] ); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/commands/renameTest.php: -------------------------------------------------------------------------------- 1 | executePugCommand( 'rename' ); 16 | 17 | $this->assertEquals( 1, $result['exit'] ); 18 | $this->assertEquals( 'usage: pug rename ', $result['output'] ); 19 | } 20 | 21 | public function testRenameNonExistentProjectReturnsError() 22 | { 23 | $this->usePugfile( '.pug-new' ); 24 | 25 | $oldProjectName = microtime( true ); 26 | $newProjectName = microtime( true ); 27 | 28 | $result = $this->executePugCommand( 'rename', [$oldProjectName, $newProjectName] ); 29 | 30 | $this->assertEquals( 1, $result['exit'] ); 31 | $this->assertEquals( "pug: No project matches '{$oldProjectName}'.", $result['output'] ); 32 | } 33 | 34 | public function testRenameProjectToExistingProjectNameReturnsError() 35 | { 36 | $this->usePugfile( '.pug-disabled' ); 37 | 38 | $oldProjectName = 'pug/2'; 39 | $newProjectName = 'pug/1'; 40 | 41 | $result = $this->executePugCommand( 'rename', [$oldProjectName, $newProjectName] ); 42 | 43 | $this->assertEquals( 1, $result['exit'] ); 44 | $this->assertEquals( "pug: A project named '{$newProjectName}' already exists.", $result['output'] ); 45 | } 46 | 47 | public function testRenameProjectReturnsListing() 48 | { 49 | $this->usePugfile( '.pug-disabled' ); 50 | 51 | $oldProjectName = 'pug/2'; 52 | $newProjectName = 'pug/3'; 53 | 54 | $result = $this->executePugCommand( 'rename', [$oldProjectName, $newProjectName] ); 55 | 56 | $this->assertEquals( 0, $result['exit'] ); 57 | 58 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 64 | } 65 | 66 | public function testRenameProjectToExistingGroupNameReturnsError() 67 | { 68 | $this->markTestIncomplete(); 69 | 70 | $this->usePugfile( '.pug-disabled' ); 71 | 72 | $oldProjectName = 'pug/2'; 73 | $newProjectName = 'pug'; 74 | 75 | $result = $this->executePugCommand( 'rename', [$oldProjectName, $newProjectName] ); 76 | 77 | $this->assertEquals( 1, $result['exit'] ); 78 | $this->assertEquals( "pug: A group named '{$newProjectName}' already exists.", $result['output'] ); 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/commands/showTest.php: -------------------------------------------------------------------------------- 1 | usePugfile( '.pug-enabled' ); 16 | $result = $this->executePugCommand( 'show' ); 17 | 18 | $this->assertEquals( 0, $result['exit'] ); 19 | 20 | $expectedOutput = <<assertEquals( $expectedOutput, $result['output'] ); 26 | } 27 | 28 | public function testShowEmptyPugfileReturnsError() 29 | { 30 | $this->usePugfile( '.pug-new' ); 31 | $result = $this->executePugCommand( 'show' ); 32 | 33 | $this->assertEquals( 0, $result['exit'] ); 34 | $this->assertEquals( "pug: Not tracking any projects. See 'pug help'", $result['output'] ); 35 | } 36 | 37 | public function testShowUnknownProjectReturnsError() 38 | { 39 | $this->usePugfile( '.pug-new' ); 40 | 41 | $targetName = microtime( true ); 42 | $result = $this->executePugCommand( 'show', [$targetName] ); 43 | 44 | $this->assertEquals( 1, $result['exit'] ); 45 | $this->assertEquals( "pug: Unknown project or directory '{$targetName}'.", $result['output'] ); 46 | } 47 | 48 | public function testShowSingleProjectReturnsListing() 49 | { 50 | $this->usePugfile( '.pug-enabled' ); 51 | $result = $this->executePugCommand( 'show', ['pug/1'] ); 52 | 53 | $this->assertSame( 0, $result['exit'] ); 54 | $this->assertSame( 0, strpos( $result['output'], '* pug/1' ) ); 55 | $this->assertSame( false, strpos( $result['output'], '* pug/2' ) ); 56 | } 57 | 58 | public function testShowGroupReturnsListing() 59 | { 60 | $this->markTestIncomplete(); 61 | 62 | $this->usePugfile( '.pug-groups' ); 63 | $result = $this->executePugCommand( 'show', ['red'] ); 64 | 65 | $this->assertSame( 0, $result['exit'] ); 66 | $this->assertTrue( strpos( $result['output'], '* red/1' ) >= 0 ); 67 | $this->assertTrue( strpos( $result['output'], '* red/2' ) >= 0 ); 68 | $this->assertFalse( strpos( $result['output'], '* green/1' ) >= 0 ); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /test/fixtures/pugfiles/.pug-disabled: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | { 4 | "name": "pug/1", 5 | "path": "./", 6 | "enabled": false, 7 | "updated": 1502560725 8 | }, 9 | { 10 | "name": "pug/2", 11 | "path": "./", 12 | "enabled": false, 13 | "updated": 1502560725 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/pugfiles/.pug-enabled: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | { 4 | "name": "pug/1", 5 | "path": "./", 6 | "enabled": true, 7 | "updated": 1502560725 8 | }, 9 | { 10 | "name": "pug/2", 11 | "path": "./", 12 | "enabled": true, 13 | "updated": 1502560725 14 | } 15 | ] 16 | } 17 | -------------------------------------------------------------------------------- /test/fixtures/pugfiles/.pug-groups: -------------------------------------------------------------------------------- 1 | { 2 | "projects": [ 3 | { 4 | "name": "red/1", 5 | "path": "./", 6 | "enabled": true, 7 | "updated": 1502560725 8 | }, 9 | { 10 | "name": "green/1", 11 | "path": "./", 12 | "enabled": true, 13 | "updated": 1502560725 14 | }, 15 | { 16 | "name": "red/2", 17 | "path": "./", 18 | "enabled": true, 19 | "updated": 1502560725 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /test/fixtures/pugfiles/.pug-new: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ashur/pug/a17b84fe90f767bc998e69652ba10c61e3670210/test/fixtures/pugfiles/.pug-new --------------------------------------------------------------------------------