-
Notifications
You must be signed in to change notification settings - Fork 1
/
git-deploy
2015 lines (1678 loc) · 78 KB
/
git-deploy
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/php
<?php
/**
* Git-deploy
* You can view this project on GitHub: https://github.com/ConnorKrammer/git-deploy-php
*/
# Define useful defaults.
define('UTILS_DIRECTORY', '.deploy/'); # Utility directory.
define('CONFIG_FILE', 'deploy-config.ini'); # Config file.
define('IGNORE_FILE', '.deployignore'); # Ignore file.
define('LOG_DIRECTORY', 'logs/'); # Log directory.
define('LOG_FILE_BASENAME', 'deploy-log-#.log'); # Log name.
define('DEPLOYMENT_REVISION', 'HEAD'); # Revision.
define('REVISION_FILE', 'REVISION'); # Remote revision file.
define('LOCAL_REVISION_FILE_BASENAME', 'LOCAL_REVISION-'); # Local revision file.
define('RESUME_FILE_BASENAME', 'RESUME-'); # Resume file.
# For updating. Change this if you're using git-deploy from a different fork.
define('GITHUB_SOURCE_URL', "https://raw.github.com/ConnorKrammer/git-deploy-php/master/git-deploy");
# Enable fnmatch flags on non-POSIX-compliant systems.
if (!function_exists('fnmatch')) {
define('FNM_PATHNAME', 1); # Slash in string only matches slash in the given pattern.
define('FNM_NOESCAPE', 2); # Disable backslash escaping.
define('FNM_PERIOD', 4); # Leading period in string must be exactly matched by period in the given pattern.
define('FNM_CASEFOLD', 16); # Caseless match.
}
# This one is custom.
define('FNM_DOUBLESTAR', 32); # Allow matching of slashes with wildcard **. Only useful with FNM_PATHNAME.
define('FNM_ALLOWSPACES', 64); # Allow matching of spaces.
# Define a more semantic exit exception.
# Used for purposefully ending app.
class ExitApp extends Exception {}
# Parse command line arguments.
$options = parseArgs();
# Define variables based on command line input.
$config = CONFIG_FILE;
$ignore = IGNORE_FILE;
$revision = $options[0] ?: DEPLOYMENT_REVISION;
$setup = isset($options['setup']); # --setup
$clean = isset($options['c']) || isset($options['clean']); # --clean
$list = isset($options['l']) || isset($options['list']); # --list
$silent = isset($options['s']) || isset($options['silent']) || $dryrun; # --silent
$nolog = isset($options['n']) || isset($options['nolog']); # --nolog
$dryrun = isset($options['d']) || isset($options['dry-run']); # --dry-run
$resumeOnly = isset($options['r']) || isset($options['resume-only']); # --resume-only
$restrictList = $options['o'] ?: $options['only'] ?: array(); # --only
$pull = $options['p'] ?: $options['pull'] ?: NULL; # --pull
$showFiltered = isset($options['f']) || isset($options['show-filtered']); # --show-filtered
$update = isset($options['u']) || isset($options['update-self']); # --update-self
$makeRevFile = $options['m'] ?: $options['make-revfile'] ?: NULL; # --make-revfile
$tags = $options['t'] ?: $options['tagged-with'] ?: array(); # --tagged-with
if (!empty($restrictList)) $restrictList = explode(' ', $restrictList);
if (!empty($makeRevFile)) $restrictList = array($makeRevFile);
if (!empty($tags)) $tags = explode(' ', $tags);
# Make sure to add a command to this list.
# If a command given isn't on this list, throw an error.
$commands = array(
'c',
'l',
's',
'n',
'd',
'r',
'o',
'p',
'f',
'u',
'm',
't',
'setup',
'clean',
'list',
'silent',
'nolog',
'dry-run',
'resume-only',
'only',
'pull',
'show-filtered',
'update-self',
'make-revfile',
'tagged-with',
);
foreach ($commands as $command) unset($options[$command]);
if ($options[0] == $revision) unset($options[0]);
if (count($options) > 0) {
foreach ($options as $key => $unrecognized) {
if (!is_numeric($key)) {
$key = strlen($key) == 1 ? '-' . $key : '--' . $key;
} else {
$key = $unrecognized;
}
echo("\nUnrecognized argument passed to git-deploy: $key");
}
echo("\nExiting.\n");
exit;
}
if ($pull && $clean) {
echo("\nYou've turned on both the --pull and --clean flags at once! Are you sure you want to do that?");
echo("\nIf you want to pull from the server, you probably don't want to clean it first. Pull first, then clean.");
echo("\nExiting.\n");
exit;
}
try {
$git = new Terra_Git(!$silent, !$nolog, $setup, $showFiltered, $dryrun);
if ($update) {
$git->updateSelf();
throw new ExitApp("Update successfully completed!");
}
if (!cmd_exists("git")) {
throw new ExitApp("Git doesn't seem to be available. Try adding it to your system PATH.");
}
if ($setup) {
$git->setup();
throw new ExitApp("Setup complete.");
}
$git->setConfigFile($restrictList, $tags);
$git->setIgnoreFile();
if ($list) $git->printAllStatuses($revision);
else if ($clean) $git->cleanAll();
else if (!empty($pull)) $git->pull($pull);
else if (!empty($makeRevFile)) $git->setRemoteRevisionFile($makeRevFile);
else $git->deployAll($revision, $resumeOnly);
} catch (ExitApp $ea) {
$git->output($ea->getMessage(), 2);
$git->output("Execution ended." . PHP_EOL, 2);
} catch (Exception $e) {
$git->output("\n\nWhoa, Nelly! We've got ourselves an unexpected exception!", 2);
$git->output("We're terminating execution. Here's the exception message:");
$message = empty($e->getMessage()) ? "No message given. Well, that was helpful." : $e->getMessage();
$git->output($message . PHP_EOL);
}
/**
* parseArgs Command Line Interface (CLI) utility function.
* @author Patrick Fisher <[email protected]>
* @see https://github.com/pwfisher/CommandLine.php
*/
function parseArgs($argv = null) {
$argv = $argv ? $argv : $_SERVER['argv']; array_shift($argv); $o = array();
for ($i = 0, $j = count($argv); $i < $j; $i++) { $a = $argv[$i];
if (substr($a, 0, 2) == '--') { $eq = strpos($a, '=');
if ($eq !== false) { $o[substr($a, 2, $eq - 2)] = substr($a, $eq + 1); }
else { $k = substr($a, 2);
if ($i + 1 < $j && $argv[$i + 1][0] !== '-') { $o[$k] = $argv[$i + 1]; $i++; }
else if (!isset($o[$k])) { $o[$k] = true; } } }
else if (substr($a, 0, 1) == '-') {
if (substr($a, 2, 1) == '=') { $o[substr($a, 1, 1)] = substr($a, 3); }
else {
foreach (str_split(substr($a, 1)) as $k) { if (!isset($o[$k])) { $o[$k] = true; } }
if ($i + 1 < $j && $argv[$i + 1][0] !== '-') { $o[$k] = $argv[$i + 1]; $i++; } } }
else { $o[] = $a; } }
return $o;
}
/**
* Checks if a given command is available on the command line.
*
* Parses the 'which' command to check if a command is available. If
* the os is Windows and the which command is unavailable, the 'where'
* command is used instead.
*
* @see http://stackoverflow.com/a/15475417/1270419
*
* @param string command The command to check for.
* @return bool True if the command is available, else false.
*/
function cmd_exists($command)
{
# Check if command exists by parsing where and which commands.
# Make sure that errors go to standard output, not standard error. (The 2>&1 bit.)
if (\strtolower(\substr(PHP_OS, 0, 3)) === 'win') {
$fp = \popen("where $command 2>&1", "r");
$result = \fgets($fp, 255);
$exists = ! \preg_match('#Could not find files#', $result);
\pclose($fp);
} else {
$fp = \popen("which $command 2>&1", "r");
$result = \fgets($fp, 255);
$exists = ! empty($result);
\pclose($fp);
}
return $exists;
}
/**
* Formats the specified number of seconds as text.
*
* The format that will be returned is of the form
* "x hours, x minutes, x seconds". Minutes will only be included if
* seconds >= 60, and hours will only be included if seconds >= 3600.
* All words will be correctly pluralized if necessary.
*
* @param int seconds The number of seconds.
* @return string A formatted string.
*/
function secondsToWords($seconds)
{
$hours = intval(intval($seconds) / 3600);
$minutes = bcmod((intval($seconds) / 60), 60);
$seconds = bcmod(intval($seconds), 60);
$words = "";
if ($hours > 0) {
$words .= pluralize($hours, "$hours hour, ", "$hours hours, ");
}
if ($hours > 0 || $minutes > 0) {
$words .= pluralize($minutes, "$minutes minute, ", "$minutes minutes, ");
}
$words .= pluralize($seconds, "$seconds second", "$seconds seconds");
return $words;
}
/**
* fnmatch() for non-POSIX-compliant systems. Not comprehensively tested.
*
* @see http://ca3.php.net/manual/en/function.fnmatch.php#100207
*
* @param string pattern The pattern to use for matching.
* @param string string The string to match against.
* @param int flags Optional flags to alter operation.
* @return bool True if the given string matched.
*/
function pcre_fnmatch($pattern, $string, $flags = 0)
{
$modifiers = null;
$transforms = array(
'\*' => '.*',
'\?' => '.',
'\[\!' => '[^',
'\[' => '[',
'\]' => ']',
'\.' => '\.',
);
// Forward slash in string must be in pattern:
if ($flags & FNM_PATHNAME) {
$transforms['\*'] = '[^/]*';
}
// Back slash should not be escaped:
if ($flags & FNM_NOESCAPE) {
unset($transforms['\\']);
}
// Perform case insensitive match:
if ($flags & FNM_CASEFOLD) {
$modifiers .= 'i';
}
// Period at start must be the same as pattern:
if ($flags & FNM_PERIOD) {
if (strpos($string, '.') == 0 && strpos($pattern, '.') !== 0) return false;
}
// Match slashes with ** even if FNM_PATHNAME is on:
if ($flags & FNM_DOUBLESTAR) {
$transforms['\*\*'] = '.*';
}
// Allow excaping spaces:
if ($flags & FNM_ALLOWSPACES) {
$transforms[' '] = '\ ';
}
# Escape all regex-related characters, then transform the relevant ones back.
$pattern = '#^'
. strtr(preg_quote($pattern, '#'), $transforms)
. '$#'
. $modifiers;
return (boolean)preg_match($pattern, $string);
}
/**
* Returns a pluralized form of a word.
*
* Singular is defined as being equal to one. This doesn't localize
* well, but this script is English-only at the moment. The gettext
* extension would be appropriate, but overkill.
*
* @param int count The number to pluralize with.
* @param string singular The singular form of the word.
* @param string plural The plural form of the word.
* @return string $singular if $count == 1, else $plural.
*/
function pluralize($count, $singular, $plural) {
return $count == 1 ? $singular : $plural;
}
class Terra_Git {
/**
* The start time of deployment.
*
* Used for informational purposes and to uniquely
* identify the log file.
*/
protected $startTime;
/**
* Whether to print output to the command line or not.
*/
protected $print = true;
/**
* Whether to record output in a log file or not.
*/
protected $log = true;
/**
* A buffer to temporarily hold log output.
* @see $this->output()
*/
protected $logBuffer = "";
/**
* Whether to send log data to the log buffer or not. Does not affect console output.
* @see $this->output()
*/
protected $logToBuffer = false;
/**
* Allows disabling output without changing the values of $print or $log.
* @see $this->output()
*/
protected $disableOutput = false;
/**
* Whether to show filtered files when parsing the ignore file.
*/
protected $showFiltered = false;
/**
* The path to the log file.
* NOTE: Currently there is no way to specify the log file's name.
*/
protected $logFile;
/**
* The path to the config file.
*/
protected $configFile;
/**
* The path to the ignore file.
*/
protected $ignoreFile;
/**
* The servers to upload to.
*
* These are specified in the configuration file,
* which defaults to deploy-config.ini under the .deploy folder.
*
* @see $this->setDeployFile() for details.
*/
protected $serverList = array();
/**
* Whether to operate in dry run mode.
* When in dry run mode, no files are changed on the FTP server,
* but all logs are recorded as if they are. This is useful for
* seeing what effect a given command would actually have.
*/
protected $dryrun = false;
/**
* Terra_Git constructor.
*
* Check for submodules and save them into an array.
* Then create the .deploy/ directory, if needed. and the log file
* for this session.
*
* @param bool print Whether to print output to the command line.
* @param bool log Whether to record output in a log file.
* @param bool setup Whether this setup is going to be run.
* @param bool dryrun Whether all commands are to be executed in dry run mode.
*/
function __construct($print = true, $log = true, $setup = false, $showFiltered = false, $dryrun = false)
{
$this->log = $log;
$this->print = $print;
$this->startTime = time();
$this->logFile = $this->getLogFilePath($this->startTime);
$this->showFiltered = $showFiltered;
$this->dryrun = $dryrun;
if (!$setup) {
$this->output("----------------------------------------", 2);
$this->output("[Starting new deploy.]");
if ($dryrun) $this->output("Dry-run mode: ON");
$this->output("Configuration file: " . $this->getConfigFilePath(CONFIG_FILE));
$this->output("Ignore file: " . $this->getIgnoreFilePath(IGNORE_FILE));
$this->output("Log file: {$this->logFile}");
$this->output("----------------------------------------");
if ($dryrun) {
$this->output("#### NOTE ####", 2);
$this->output("Git-deploy is running with the option --dry-run turned on. No changes will be made, but you");
$this->output("will be able to view the output as if they were taking place. Running --dry-run overrides");
$this->output("--silent, but not --nolog, so if the --nolog flag is set, no log files will be created.");
}
}
}
function __destruct() { }
/**
* Sets up git-deploy.
*
* Creates the utility directory, the log directory, the default
* config file, and the default ignore file. It will not create
* one of the above if it already exists. This function is run
* when calling git-deploy with the option --setup.
*/
public function setup()
{
$utils = $this->getUtilsDirectory();
$logs = $this->getLogDirectory();
$config = $this->getConfigFilePath();
$ignore = $this->getIgnoreFilePath();
# If no log file exists, temporarily use a buffer.
$buffer = !file_exists($logs) ? '' : NULL;
$this->output("----------------------------------------", 2, $buffer);
$this->output("Running setup.", 1, $buffer);
$this->output("----------------------------------------", 1, $buffer);
# Create .deploy/ directory, if needed.
$this->output("Creating .deploy/ directory... ", 2, $buffer);
if (!file_exists($utils)) {
if (!@mkdir($utils, 0755, true)) {
$this->output("Failed", 0, $buffer);
throw new ExitApp("Setup will terminate. You can try creating $utils manually, or file a bug report.");
}
$this->output("Done.", 0, $buffer);
} else {
$this->output("Not needed. $utils already exists.", 0, $buffer);
}
chmod($utils, 0755);
# Create .deploy/logs/ directory, if needed.
$this->output("Creating .deploy/logs/ directory... ", 1, $buffer);
if (!file_exists($logs)) {
if (!@mkdir($logs, 0755, true)) {
$this->output("Failed", 0, $buffer);
throw new ExitApp("Setup will terminate. You can try creating $logs manually, or file a bug report.");
}
$this->output("Done.", 0, $buffer);
} else {
$this->output("Not needed. $logs already exists.", 0, $buffer);
}
# Create .deploy/deploy-config.ini, if needed.
$this->output("Creating .deploy/deploy-config.ini... ", 1, $buffer);
if (!file_exists($config)) {
if (!file_put_contents($config, $this->getDefaultConfigFileContents())) {
$this->output("Failed.", 0, $buffer);
throw new ExitApp("Terminating. Try manually creating $config.");
}
$this->output("Done.", 0, $buffer);
} else {
$this->output("Not needed. $config already exists.", 0, $buffer);
$this->output("If you wanted to restore the default config file, delete the old one first and run --setup again.", 1, $buffer);
}
# Create .deploy/.deployignore, if needed.
$this->output("Creating .deploy/.deployignore... ", 1, $buffer);
if (!file_exists($ignore)) {
if (!file_put_contents($ignore, $this->getDefaultIgnoreFileContents())) {
$this->output("Failed.", 0, $buffer);
throw new ExitApp("Terminating. Try manually creating $ignore.");
}
$this->output("Done.", 0, $buffer);
} else {
$this->output("Not needed. $ignore already exists.", 0, $buffer);
$this->output("If you wanted to restore the default ignore file, delete the old one first and run --setup again.", 1, $buffer);
}
if (file_exists($logs)) $this->dumpBuffer($buffer);
}
/**
* Extract server settings from a configuration file.
*
* @param array restrictList An optional list of servers to restrict deployment to.
* @param array tagList An optional list of tags to restrict deployment to.
*/
public function setConfigFile($restrictList = array(), $tagList = array())
{
$this->output("----------------------------------------", 2);
$this->output("Initializing config values.");
$this->output("Deployment configuration file: $config");
$this->output("----------------------------------------");
$config = $this->getConfigFilePath();
$this->configFile = $config;
if (!@file_exists($config)) {
$this->output("Config file '$config' does not exist!", 2);
$this->output("You can restore the config file by running php git-deploy --setup.");
throw new ExitApp();
}
$serverList = parse_ini_file($config, true);
if (!$serverList) {
throw new ExitApp("Config file '$config' is not a valid .ini file.");
}
$this->addServers($serverList, $restrictList, $tagList);
}
/**
* Setup the ignore file.
*/
public function setIgnoreFile()
{
$this->ignoreFile = $this->getIgnoreFilePath();
}
/**
* Return the default deploy-config.ini file.
*
* The default configuration file is stored in code because that way it can
* always be restored if deleted by accident. It is formatted with quotes then
* cleaned of leading whitespace so as not to ruin indentation too much. (People before machines.)
*/
protected function getDefaultConfigFileContents()
{
$default = "; -----------------------------------------------------------------------------
; This is the default deployment config file. Replace options below with values
; relavant to you. If this file is ever deleted and you don't remember what was
; required for it to work, just run git-deploy and a new deploy.ini will be
; generated with default values filled out and comments restored. Feel free to
; remove the comments if you don't need them.
; -----------------------------------------------------------------------------
; Server identifier. Required.
; You may specify more than one server, each with its own unique identifier and options.
[example]
; You can set keywords ('tags') for servers. That way you can tag some servers with
'production' or 'development', for example, and only deploy to servers with the matching tags.
tags[] = example
tags[] = development
; Whether to skip this configuration section.
; This is only set to true for the example.
; ### SET THIS TO FALSE WHEN DEPLOYING! ###
skip = true
; Shortform. This declaration combines the username, password, host,
; port, and path in one line.
short: ftp://username:[email protected]:21/path/to/installation
; The FTP username to login as.
; May be the same as your hosting or cPanel username.
user = ftp_user
; The FTP password to login with.
; May be the same as your hosting or cPanel password.
pass = ftp_pass
; The FTP host to connect to.
; If you own your own domain name you can generally just enter that.
; Otherwise, most hosting providers (godaddy, bluehost, etc.) have FAQ
; pages you can check out. A google search can also be informative.
host = example.com
; The port to use for the FTP connection, if the connection mode is active.
; Most servers are configured to use port 21 and connect back to you on port 20.
; If using active mode, make sure this port is open in your firewall.
; Defaults to 21;
port = 21
; Whether to use active or passive connection modes.
; If true, use passive, else use active. Defaults to true.
; Passive mode is generally useful if you are behind a firewall.
; If you don't understand the difference, this StackOverflow answer
; will help: http://stackoverflow.com/a/1699163/1270419
passive = true
; A path relative to the home directory on the server where files should be uploaded to.
path = upload/destination/
; Directories to empty on each deployment. The directory itself will not be removed.
; Useful for clearing out temporary files, for example.
clean_directories[] = 'temp'
clean_directories[] = 'cache'";
# Strip leading whitespace from each line and return
return join("\n", array_map("trim", explode("\n", $default)));
}
/**
* Return the default .deployignore file.
*
* The .deployignore file specifies which files and directories should not be uploaded.
* The default file is stored in code because that way it can always be restored
* if deleted by accident. It is formatted with quotes then cleaned of leading
* whitespace so as not to ruin indentation too much. (People before machines.)
*
* The .deployignore file responds to the same syntax as a .gitignore file.
* @see http://git-scm.com/docs/gitignore
*/
protected function getDefaultIgnoreFileContents()
{
$default = "# Feel free to delete this massive comment section.
# Description:
# This is the default .deployignore file. It functions similar to a .gitignore file,
# (and has an almost identical syntax, with the addition of optional regexes) except
# that instead of excluding files from version control, it excludes files from being
# deployed to a server. Since files not tracked by git aren't included in deployment
# anyway, you don't need to duplicate your .gitignore file here.
# Function:
# > Blank lines are ignored.
# > Lines beginning with # are ignored.
# > Patterns starting with ! negate any matches.
# > Negations have a higher precedence, and will not be overridden by following exclusions (unlike .gitignore).
# > Each line can only contain one pattern.
# > Each pattern uses the fnmatch() php function to match filenames, or regex, if turned on.
# > Patterns are searched relative to git-deploy's location.
# > If you want to match a filepath that starts with # or !, you can escape the character with a backslash.
# Available wildcards:
# > Match everything except '/': *
# > Match any single character: ?
# > Match everything: **
# > Match * or ? or ** characters: [?] or [*] or [*][*]
# > Match any character in letters: [letters]
# > Match any character not in letters: [!letters]
# Using regex:
# > You can toggle regex patterns by typing 'regex on' or 'regex off' on its own line. All
# > patterns coming after a 'regex on' line are parsed as regex. Lines after 'regex off' are
# > parsed in .gitignore style again. The regex toggle line is case insensitive, so 'REGEX OFF'
# > works just as well as 'regex off' or 'ReGeX oFf'. (Please don't use thet last one!)
# >
# > Note that with regex, the ^ anchor character will anchor to the beginning of the path relative to
# > the git-deploy file, so ^hello.txt$ matches hello.txt only when it is in the root directory.
# Examples:
# > Exclude every file: **
# > Exclude all .tmp files: *.tmp
# > Exclude all files within dir: dir/
# > ...except .php files: !dir/*.php
# > Exclude bat, hat, cat, etc: ?at
# > Exclude .txt files in root directory: /*.txt
# .deployignore's syntax works the same as .gitignores, minus bugs. (Report them!)
# See git-scm.com/docs/gitignore/#_pattern_format for how .gitignore pattern format works.
# The patterns below are hard-coded into this script for security reasons. You can safely remove them in this file.
# git-deploy*
# .deploy/
# basename(__FILE__)* # This one is evaluated to whatever this script's name is.";
# Strip leading whitespace from each line and return
return join("\n", array_map("trim", explode("\n", $default)));
}
/**
* Adds a server to the deployment list.
*
* The $serverList argument is an array of different server configurations,
* each set up using the options as defined in the config file. Two syntaxes
* are allowed: The shortform (An FTP url starting with ftp:// ), or the name
* of the server followed by all the options. The options in the config file
* all go by the same names as those returned by parse_url(), so array_merge()
* is used to combine them. The same process is used to define some useful defaults.
*
* @param array serverList An associative array of server identifiers to server configuration options.
* @param array restrictList A list of server identifiers to deploy. All others are excluded.
* @param array tagList A list of server tags to deploy. All others are excluded.
*/
protected function addServers($serverList, $restrictList = array(), $tagList = array())
{
$this->output("Selecting servers...", 2);
$removeList = !empty($restrictList)
? array_diff(array_keys($serverList), $restrictList)
: array();
if (!empty($tagList)) {
$filteredTags = preg_grep("/^!.*$/", $tagList);
$allowedTags = array_diff($tagList, $filteredTags);
foreach ($filteredTags as $key => $tag) $filteredTags[$key] = ltrim($tag, '!');
foreach($serverList as $name => $server) {
$matches = array_intersect($allowedTags, $server['tags']);
$filteredMatches = array_intersect($filteredTags, $server['tags']);
if (count($matches) == 0 || count($filteredMatches) != 0) {
$removeList[] = $name;
}
}
}
foreach (array_unique($removeList) as $name) {
$this->output(" " . "Skipping server with identifier \"$name\"", 2);
$this->output(" " . "Tags: ");
if (count($serverList[$name]['tags']) > 0) {
foreach($serverList[$name]['tags'] as $tag) $this->output("[$tag] ", 0);
} else {
$this->output("none");
}
unset($serverList[$name]);
}
if (count($serverList) == 0) throw new ExitApp("No servers selected for deployment.");
foreach ($serverList as $name => $server) {
$this->output(" " . "Adding server with identifier \"$name\"", 2);
$this->output(" " . "Tags: ");
if (count($server['tags']) > 0) {
foreach($server['tags'] as $tag) $this->output("[$tag] ", 0);
} else {
$this->output("none");
}
# If the shortform syntax is used in the config file.
if (@substr($server['short'], 0, 6) == 'ftp://') {
$server = array_merge($server, parse_url($server['short']));
}
# Throw in some default values, in case they're not set.
$server = array_merge(array(
'skip' => false,
'host' => '',
'user' => '',
'pass' => '',
'port' => 21,
'path' => '/',
'passive' => true,
'clean_directories' => array(),
'tags' => array(),
), $server);
# This serves as an identifier for LOCAL_REVISION files.
$server['name'] = $name;
# Skip if configured, else assign to $this->serverList.
if ($server['skip']) {
$this->output("'skip' config value set to true.", 2);
$this->output("Skipping deployment to server with identifier $name}.");
continue;
}
$this->serverList[$name] = $server;
}
}
/**
* Prints all the changes pending on all servers.
*
* @param string revision The revision to list changes to.
*/
public function printAllStatuses($revision)
{
foreach ($this->serverList as $server) {
$this->printServerStatus($revision, $server);
}
}
/**
* Deploys a revision to all servers.
*
* @param string revision The revision to deploy.
* @param bool resumeOnly Whether to stop after resuming.
*/
public function deployAll($revision, $resumeOnly)
{
foreach ($this->serverList as $server) {
$connection = $this->connect($server);
if (!$connection) continue;
$this->deploy($connection, $server, $revision, $resumeOnly);
ftp_close($connection);
}
}
/**
* Cleans all servers' upload directories.
*/
public function cleanAll()
{
foreach ($this->serverList as $server) {
$connection = $this->connect($server);
if (!$connection) continue;
$this->clean($connection, $server);
ftp_close($connection);
}
$this->output("----------------------------------------", 2);
$this->output("Cleaning local files...");
$this->output("----------------------------------------");
foreach ($this->serverList as $server) {
$localRevisionPath = $this->getRemoteRevisionLocalFilePath($server['name']);
if (file_exists($localRevisionPath)) {
$this->output("LOCAL_REVISION file found for {$server['name']}. Removing... ", 2);
if (!$this->dryrun && !@unlink($localRevisionPath)) {
$this->output("Failed.", 0);
throw new ExitApp("Couldn't remove $localRevisionPath. Try doing it manually.");
}
$this->output("Done.", 0);
}
$resumeFile = $this->getResumeFilePath($server['name']);
if (file_exists($resumeFile)) {
$this->output("RESUME file found for {$server['name']}. Removing... ");
if (!$this->dryrun && !@unlink($resumeFile)) {
$this->output("Failed.", 0);
throw new ExitApp("Couldn't remove $resumeFile. Try doing it manually.");
}
$this->output("Done.", 0);
}
}
}
/**
* Returns a list of the files and their statuses.
*
* Runs and parses git-diff to get all files and how they would change if the given
* revision were uploaded. The return format is an array of filename-status pairs, one
* per index. The status is indicated by a single character that is the same as those used
* by git-diff. If the $oldRevision parameter is set to NULL, all files will be returned
* with the status indicator 'A', for 'add'.
*
* Files that match the shell glob patterns found within the configured ignorefile
* will be excluded from the returned array of files.
*
* @see http://stackoverflow.com/a/8691226/1270419 for a list of git-diff statuses.
*
* @param string oldRevision The revision the changes will be relative to.
* @param string revision The revision to compare with.
* @param array cleanDirectories Optional directories to empty that are not tracked by git.
* @return array All files that different in newRevision compared to oldRevision.
*/
protected function getFileStatusList($oldRevision = NULL, $revision = 'HEAD', $cleanDirectories = array())
{
$cleanFiles = array();
foreach ($cleanDirectories as $directory) {
$status = 'clean';
$path = $directory;
$cleanFiles[] = array('path' => $path, 'status' => $status);
}
if (empty($revision)) return $cleanFiles;
$output = array();
$command = empty($oldRevision)
? "git ls-tree -r --name-only $revision"
: "git diff --name-status $oldRevision $revision";
$this->runGitCommand($command, $output);
$files = array();
foreach ($output as $line) {
$status = empty($oldRevision) ? 'A' : substr($line, 0, 1);
$path = empty($oldRevision) ? trim($line) : trim(substr($line, 1));
$files[] = array('path' => $path, 'status' => $status);
}
$this->output("Parsing .deployignore...", 2);
$ignoreFile = @file($this->ignoreFile);
$ignorePatterns = array();
$includePatterns = array();
$useRegex = false;
foreach ($ignoreFile as $line) {
$line = trim($line);
$firstChar = substr($line, 0, 1);
# It's a comment or a blank line.
if (empty($line) || $firstChar == '#') continue;
# Toggle regex values.
if (strtolower($line) == 'regex on') $useRegex = true;
else if (strtolower($line) == 'regex off') $useRegex = false;
# It's an include pattern, else an ignore pattern.
# Ignore patterns are stripped of leading escape characters.
if ($firstChar == '!') $includePatterns[] = array('pattern' => substr($line, 1), 'regex' => $useRegex);
else $ignorePatterns[] = array('pattern' => ltrim($line, '\\'), 'regex' => $useRegex);
}
# These ignore patterns are hard-coded for security purposes.
$masterPatterns = array();
$masterPatterns[] = 'git-deploy*';
$masterPatterns[] = basename(__FILE__) . '*';
$masterPatterns[] = '.deploy/';
foreach($ignorePatterns as $pattern) $this->output(" " . "Ignore pattern: " . $pattern['pattern']);
foreach($includePatterns as $pattern) $this->output(" " . "Negate pattern: " . $pattern['pattern']);
$this->output("Filtering files...", 2);
$padSize = 0;
$ignoreCount = 0;
# For formatting purposes only.
foreach (array_merge($ignorePatterns, $includePatterns) as $pattern) {
$padSize = max($padSize, strlen($pattern['pattern']));
}
foreach ($files as $key => $file) {
$path = $file['path'];
$ignore = false;
$isMasterIgnored = false;
# Exclude security risks.
foreach ($masterPatterns as $master) {
if ($this->matchFile($master, $path)) {
$isMasterIgnored = true;
if ($this->showFiltered) {
$this->output(" " . '[Excluded with master pattern] ' . str_pad($master, $padSize, ' ') . " => " . $path);
}
}
}
if ($isMasterIgnored) {
unset($files[$key]);
$ignoreCount++;
continue;
}
foreach ($ignorePatterns as $pattern) {
if ($this->matchFile($pattern['pattern'], $path, $pattern['regex'])) {
if ($this->showFiltered) {
$this->output(" " . '[Excluded with pattern] ' . str_pad($pattern['pattern'], $padSize, ' ') . " => " . $path);
}
$ignore = true;
break;
}
}
if (!$ignore) continue;
foreach ($includePatterns as $pattern) {
if ($this->matchFile($pattern['pattern'], $path, $pattern['regex'])) {
if ($this->showFiltered) {
$this->output(" " . '[Included again with pattern] ' . str_pad($pattern['pattern'], $padSize, ' ') . " => " . $path);
}
$ignore = false;
break;
}
}
if ($ignore) {
unset($files[$key]);
$ignoreCount++;