1 #!/usr/bin/perl
2
3 use strict;
4 use warnings;
5
6 use Getopt::Long qw(GetOptions);
7 use Log::Log4perl qw(:easy);
8 # TODO: This needs to be scoped properly
9 use lib "/usr/local/lib";
10 use Shellex::Shellex qw(shellex findBin);
11 use SimplyGit::Git qw(
12 readConfig getStatus returnState addFiles
13 commitChanges pushChanges stashAndReset resetFromUpstream
14 updateGitIgnore appendRepoUserConfig parseSGConfig
15 warnOnUser basicClone basicPull knocker
16 );
17
18 sub initSG($) {
19
20 my $sgDir = shift;
21 my $homeDir = shellex("echo \$HOME");
22 chomp $homeDir;
23 my $path = $homeDir . "/" . $sgDir;
24 my $logFile = $homeDir . "/" . $sgDir . "/" . "sgLog.txt";
25 my $configFile = $homeDir . "/" . $sgDir . "/" . "sg.config";
26 if ( ! -d $path ) {
27 print "Creating $path\n";
28 shellex("mkdir $path");
29 }
30
31 if ( ! -f $logFile ) {
32 print "Creating $logFile\n";
33 shellex("touch $logFile");
34 }
35
36 if ( ! -f $configFile ) {
37 print "Creating $configFile\n";
38 shellex("touch $configFile");
39 }
40
41 return ( $path, $logFile, $configFile );
42
43 }
44
45 my ( $sgPath, $sgLogFile, $sgConfigFile ) = initSG(".sg");
46 sub getLogName { return $sgLogFile; };
47 my $log_conf = q(
48 log4perl.rootLogger = ERROR, LOG1, screen
49 log4perl.appender.LOG1 = Log::Log4perl::Appender::File
50 log4perl.appender.LOG1.filename = sub { getLogName(); }
51 log4perl.appender.LOG1.mode = append
52 log4perl.appender.LOG1.layout = Log::Log4perl::Layout::PatternLayout
53 log4perl.appender.LOG1.layout.ConversionPattern = %d %p >> %m %n
54
55 log4perl.appender.screen = Log::Log4perl::Appender::Screen
56 log4perl.appender.screen.stderr = 0
57 log4perl.appender.screen.layout = PatternLayout
58 log4perl.appender.screen.layout.ConversionPattern = %d %p >> %m %n
59 );
60
61 Log::Log4perl::init(\$log_conf);
62 my $logger = get_logger();
63 my $gitCmd = findBin("git",$logger);
64 # Removed .sg from repo dir to home, don't need this sub right now
65 # updateGitIgnore(".","/.sg",$logger);
66
67 my %args;
68 GetOptions(
69 \%args,
70 'push-all',
71 'interactive',
72 'view',
73 'reset-from-master',
74 'reset-from-upstream',
75 'upstream-url=s',
76 'commit-msg=s',
77 'dump-config',
78 'configure-local-user',
79 'user=s',
80 'email=s',
81 'config-file=s',
82 'knock',
83 'knock-clone=s',
84 'help',
85 'knock-pull',
86 );
87
88 # TODO: This should maybe be more robust?
89 if ( ! -d ".git" && ( ! defined $args{'knock-clone'} && ! defined $args{'knock'} && ! defined $args{'help'} ) ) {
90 print "Not a git dir, exiting...\n";
91 exit 1;
92 }
93
94 sub printHelp {
95
96 my $help = <<EOF
97 simply-git
98 Usage:
99 --view
100 Display git status of files and other information
101
102 --dump-config
103 Dump .git/config to STDOUT. Not really useful but exposed for testing of reading config into internal data structure
104
105 --push-all [--commit-msg]
106 Push all untracked and modified files
107 * Can be used with interactive mode
108 * Can provide a commit msg with --commit-msg (otherwise a generic will be provided)
109
110 --interactive
111 Enable interactive mode with supported opts
112
113 --reset-from-master
114 Reset all current changes so that the file tree matches origin master
115
116 --reset-from-upstream [ --upstream-url ]
117 If upstream is defined will reset local branch to match upstream ( does not push changes to origin )
118 * Assumes you have an upstream configured
119 * Pass SSH/HTTPS URL to --upstream-url to add an upstream
120
121 --configure-local-user [--user,--email]
122 Configure local git user
123 * Can be used with interactive mode
124
125 --config-file
126 Default is ~/.sg/sg.config, can use this opt to use another file
127 * See example.config
128
129 --knock
130 Will try and knock the defined git server at the defined ports before any operation
131 * See example.config
132 * Can pass this by itself to perform a knock and exit
133
134 --knock-clone
135 Will try and knock the defined git server and clone the provided repo
136 * Will not check if you're in a git dir
137
138 --knock-pull
139 Will try and knock the defined git server and git pull
140
141 EOF
142 ;
143
144 print "$help\n";
145
146 }
147
148 if ( scalar keys %args < 1 ) {
149 printHelp();
150 }
151
152 ###
153 # TODO: This args processing is better and more predictable than it was, bit there is still
154 # likely a lot of room for improvement...
155 ###
156
157 sub parseArgs {
158
159 if ( defined $args{'help'} ) {
160 printHelp();
161 exit 0;
162 }
163
164 if ( defined $args{'view'} && scalar keys %args > 1 ) {
165 print "Can't pass other args with --view\n";
166 exit 1;
167 }
168
169 if ( defined $args{'dump-config'} && scalar keys %args > 1 ) {
170 print "Can't pass other args with --dump-config\n";
171 exit 1;
172 }
173
174 if ( defined $args{'reset-from-master'} && scalar keys %args > 1 ) {
175 print "Can't pass other args with --reset-from-master\n";
176 exit 1;
177 }
178
179 if ( defined $args{'push-all'} ) {
180 foreach my $arg ( keys %args ) {
181 if ( $arg eq "interactive" || $arg eq "commit-msg" || $arg eq "push-all" || $arg eq "knock" ) {
182 next;
183 } else {
184 print "Can only pass --interactive and --commit-msg with --push-all\n";
185 exit 1;
186 }
187 }
188 }
189
190 if ( defined $args{'configure-local-user'} ) {
191 if ( scalar keys %args < 2 ) {
192 print "Must pass either --interactive or --user AND --email to --configure-local-user\n";
193 exit 1;
194 }
195
196 foreach my $arg ( keys %args ) {
197 if ( $arg eq "interactive" || $arg eq "user" || $arg eq "email" || $arg eq "configure-local-user" ) {
198 next;
199 } else {
200 print "Must/can only pass --interactive, OR --user AND --email with --configure-local-user\n";
201 exit 1;
202 }
203 }
204
205 if ( ! defined $args{'interactive'} && ! defined $args{'user'} || ! defined $args{'interactive'} && ! defined $args{'email'} ) {
206 print "If not using --interactive with --configure-local-user, --user and --email MUST be defined\n";
207 exit 1;
208 }
209 }
210
211 if ( defined $args{'reset-from-upstream'} ) {
212 if ( scalar keys %args > 2 ) {
213 print "Can only pass --upstream-url with --reset-from-upstream\n";
214 exit 1;
215 }
216
217 foreach my $arg ( keys %args ) {
218 if ( $arg eq "reset-from-upstream" || $arg eq "upstream-url" ) {
219 next;
220 } else {
221 print "Can only pass --upstream-url with --reset-from-upstream\n";
222 exit 1;
223 }
224 }
225 }
226
227 if ( ! defined $args{'config-file'} ) {
228 $args{'config-file'} = $sgConfigFile;
229 }
230
231 if ( defined $args{'knock-clone'} ) {
232 if ( scalar keys %args > 2 ) {
233 print "--knock-clone accepts no other args\n";
234 exit 1;
235 }
236 }
237
238 if ( defined $args{'knock-pull'} ) {
239 if ( scalar keys %args > 2 ) {
240 print "--knock-pull accepts no other args\n";
241 exit 1;
242 }
243 }
244
245 }
246
247 sub color_print($$) {
248
249
250 #echo -e "\033[0mNC (No color)"
251 #echo -e "\033[1;37mWHITE\t\033[0;30mBLACK"
252 #echo -e "\033[0;34mBLUE\t\033[1;34mLIGHT_BLUE"
253 #echo -e "\033[0;32mGREEN\t\033[1;32mLIGHT_GREEN"
254 #echo -e "\033[0;36mCYAN\t\033[1;36mLIGHT_CYAN"
255 #echo -e "\033[0;31mRED\t\033[1;31mLIGHT_RED"
256 #echo -e "\033[0;35mPURPLE\t\033[1;35mLIGHT_PURPLE"
257 #echo -e "\033[0;33mYELLOW\t\033[1;33mLIGHT_YELLOW"
258 #echo -e "\033[1;30mGRAY\t\033[0;37mLIGHT_GRAY"
259 #
260 # Doing it this way likely hurts portability but
261 # it's better than nothing which is what I'm currently doing
262 # and would like to avoid additional module deps, as this
263 # isn't too hard to implement
264
265 my $print_string = shift;
266 my $color = shift;
267
268 if ( $color ne "BLUE" && $color ne "GREEN" && $color ne "RED" ) {
269 $logger->error("Bad color passed to color_print");
270 exit 1;
271 }
272
273 my %color_map = (
274 BLUE => "\033[0;34m",
275 GREEN => "\033[0;32m",
276 RED => "\033[0;31m",
277 RESET => "\033[0m",
278 );
279
280 printf "$color_map{$color}$print_string$color_map{'RESET'}";
281
282 }
283
284
285 parseArgs();
286 my %sgConfig = parseSGConfig($args{'config-file'},$logger);
287 if ( defined $sgConfig{'UserWarn'} && -d ".git" ) {
288 warnOnUser($sgConfig{'user.name'},$sgConfig{'user.email'},$logger);
289 }
290
291 sub knock() {
292 if ( defined $sgConfig{'Knock'} && ( defined $args{'knock'} || defined $args{'knock-clone'} || defined $args{'knock-pull'} ) ) {
293 knocker($sgConfig{'knock.target'},$sgConfig{'ports'},$logger);
294 }
295 }
296
297 if ( defined $args{'knock'} && scalar keys %args == 2 ) {
298 print "Just knocking then exiting...\n";
299 knock();
300 exit 1;
301 }
302
303 # TODO: This sub could be more concise with a sub to print array refs
304 if ( defined $args{'view'} ) {
305 my ( $untrackedRef, $modifiedRef, $addedRef, $deletedRef ) = returnState($logger);
306 #my $refs = shellex("$gitCmd show-ref",$logger);
307 my $branch = shellex("$gitCmd show-branch",$logger);
308 my $name = shellex("$gitCmd config --get user.name",$logger);
309 chomp $name;
310 my $email = shellex("$gitCmd config --get user.email",$logger);
311 chomp $email;
312 color_print("-->Username: $name\n-->Email: $email\n","BLUE");
313 print "Branches:\n";
314 color_print("$branch\n","GREEN");
315 #print "$refs\n";
316 print "Files:\n";
317 my $swpWarning = "\t# Likely a Vi .swp file";
318
319 my $untrackedTotal = scalar @$untrackedRef;
320 print "* $untrackedTotal untracked file(s):\n";
321 foreach my $file ( @$untrackedRef ) {
322 if ( $file =~ m/.swp/ ) {
323 color_print("\t$file $swpWarning\n","GREEN");
324 } else {
325 color_print("\t$file\n","GREEN");
326 }
327 }
328
329 my $modifiedTotal = scalar @$modifiedRef;
330 print "* $modifiedTotal modified file(s):\n";
331 foreach my $file ( @$modifiedRef ) {
332 if ( $file =~ m/.swp/ ) {
333 color_print("\t$file $swpWarning\n","GREEN");
334 } else {
335 color_print("\t$file\n","GREEN");
336 }
337 }
338
339 my $commitTotal = scalar @$addedRef;
340 print "* $commitTotal file(s) added to commit:\n";
341 foreach my $file ( @$addedRef ) {
342 if ( $file =~ m/.swp/ ) {
343 print "\t$file $swpWarning\n";
344 } else {
345 print "\t$file\n";
346 }
347 }
348
349 my $deletedTotal = scalar @$deletedRef;
350 print "* $deletedTotal file(s) to be deleted from commit:\n";
351 foreach my $file ( @$deletedRef ) {
352 if ( $file =~ m/.swp/ ) {
353 color_print("\t$file $swpWarning\n","RED");
354 } else {
355 color_print("\t$file\n","RED");
356 }
357 }
358 }
359
360 if ( defined $args{'push-all'} ) {
361
362 my ( $untrackedRef, $modifiedRef ) = returnState($logger);
363 my @files;
364 push(@files,@$untrackedRef); push(@files,@$modifiedRef);
365 my @filesToCommit;
366 if ( defined $args{'interactive'} ) {
367 foreach my $file ( @files ) {
368 print "Add $file to commit (y/n): ";
369 my $input = <STDIN>;
370 chomp $input;
371 if ( $input =~ m/^Y|^y/ ) {
372 push(@filesToCommit,$file);
373 } else {
374 next;
375 }
376 }
377
378 } else {
379 @filesToCommit = @files;
380 }
381
382 print "Commiting the following files:\n";
383 foreach my $file ( @filesToCommit ) {
384 print "\t$file\n";
385 }
386
387 if ( defined $args{'interactive'} ) {
388 print "Does this look correct (y/n) : ";
389 my $input = <STDIN>;
390 chomp $input;
391 if ( $input !~ m/^Y|^y/ ) {
392 print "Canceling...\n";
393 exit 1;
394 }
395 }
396
397 addFiles(\@filesToCommit,$logger);
398
399 if ( defined $args{'interactive'} && ! defined $args{'commit-msg'}) {
400 print "Enter a commit message: ";
401 my $input = <STDIN>;
402 chomp $input;
403 commitChanges($input,$logger);
404 } elsif ( defined $args{'commit-msg'} ) {
405 my $commitMsg = "$args{'commit-msg'}";
406 commitChanges($commitMsg,$logger);
407 } else {
408 my $epoch = time();
409 my $commitMsg = "Generic Commit at $epoch";
410 commitChanges($commitMsg,$logger);
411 }
412
413 if ( defined $args{'interactive'} ) {
414 print "Push changes? (y/n): ";
415 my $input = <STDIN>;
416 chomp $input;
417 if ( $input !~ m/^Y|^y/ ) {
418 # TODO: Unstage changes?
419 print "Canceling...\n";
420 exit 1;
421 }
422
423 knock();
424 my $gitOutput = pushChanges($logger);
425 print "Git returned:\n$gitOutput\n";
426
427 }
428
429 else {
430
431 knock();
432 pushChanges($logger);
433 my $gitOutput = pushChanges($logger);
434 print "Git returned:\n$gitOutput\n";
435
436 }
437
438
439 }
440
441 if ( defined $args{'reset-from-master'} ) {
442
443 knock();
444 stashAndReset($logger);
445
446 }
447
448 if ( defined $args{'reset-from-upstream'} ) {
449
450 if ( defined $args{'upstream-url'} ) {
451 print "Setting upstream to $args{'upstream-url'}\n";
452 chomp $args{'upstream-url'};
453 shellex("$gitCmd remote add upstream $args{'upstream-url'}",$logger);
454 shellex("$gitCmd fetch upstream",$logger);
455 knock();
456 resetFromUpstream($logger);
457 } else {
458 knock();
459 resetFromUpstream($logger);
460 }
461 }
462
463 if ( defined $args{'dump-config'} ) {
464
465 my %configHash = readConfig(".",$logger);
466 foreach my $key ( keys %configHash ) {
467 my $hRef = $configHash{$key};
468 print "[$key]\n";
469 foreach my $ckey ( keys %$hRef ) {
470 print "\t$ckey = ${$hRef}{$ckey}\n";
471 }
472 }
473
474 }
475
476 if ( defined $args{'configure-local-user'} ) {
477
478 if ( defined $args{'interactive'} ) {
479 print "Enter user to set: ";
480 my $desiredName = <STDIN>;
481 chomp $desiredName;
482 print "Enter email to set: ";
483 my $desiredEmail = <STDIN>;
484 chomp $desiredEmail;
485
486 appendRepoUserConfig($desiredName,$desiredEmail,$logger);
487
488 } else {
489
490 appendRepoUserConfig($args{'user'},$args{'email'},$logger);
491
492 }
493
494 }
495
496 if ( defined $args{'knock-clone'} ) {
497 knock();
498 basicClone($args{'knock-clone'},$logger);
499 }
500
501 if ( defined $args{'knock-pull'} ) {
502 knock();
503 basicPull($logger);
504 }