forked from dfabulich/easygit
-
Notifications
You must be signed in to change notification settings - Fork 2
/
eg
executable file
·7686 lines (6625 loc) · 268 KB
/
eg
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/perl -w
## Easy GIT (eg), a frontend for git designed for former cvs and svn users
## Version 1.8.5
## Copyright 2008-2013 by Elijah Newren and others
## Licensed under GNU GPL, version 2.
## To use eg, simply stick this file in your path. Then fire off an
## 'eg help' to get oriented. You may also be interested in
## http://www.gnome.org/~newren/eg/git-for-svn-users.html
## to get a comparison to svn in terms of capabilities and commands.
## Webpage for eg: http://www.gnome.org/~newren/eg
use strict;
use warnings;
package main;
use Getopt::Long;
use Cwd qw(getcwd abs_path);
use List::Util qw(max);
use File::Temp qw/ tempfile /;
# configurables
my $DEBUG = $ENV{EASYGIT_DEBUG} || 0;
my $GIT_CMD = $ENV{EASYGIT_GIT_CMD} || "git"; # Includes any global args, thus "git --exec-path=..."
my $USE_PAGER = exists($ENV{EASYGIT_USE_PAGER}) ? $ENV{EASYGIT_USE_PAGER} : -1;
# globals :-(
my $OUTFH;
my $VERSION = "1.8.5";
my $EG_EXEC = abs_path($0);
my %ALIAS;
my %COMMAND; # command=>{section, short_description} mapping
my $SECTION = {
'creation' =>
{ order => 1,
desc => 'Creating repositories',
},
'discovery' =>
{ order => 2,
desc => 'Obtaining information about changes, history, & state',
},
'modification' =>
{ order => 3,
desc => 'Making, undoing, or recording changes',
},
'projects' =>
{ order => 4,
desc => 'Managing branches',
},
'collaboration' =>
{ order => 5,
desc => 'Collaboration'
},
'timesavers' =>
{ order => 6,
desc => 'Time saving commands'
},
'compatibility' =>
{ order => 7,
extra => 1,
desc => 'Commands provided solely for compatibility with other ' .
'prominent SCMs'
},
'misc' =>
{ order => 8,
extra => 1,
desc => 'Miscellaneous'
},
};
my ($CURDIR, $TOPDIR, $GITDIR);
## Commands to list in help even though we haven't overridden the git versions
## (yet, in most cases)
INIT {
%COMMAND = (
blame => {
unmodified_help => 1,
unmodified_behavior => 1,
extra => 1,
section => 'discovery',
about => 'Show what version and author last modified each line of a file'
},
bisect => {
unmodified_help => 1,
unmodified_behavior => 1,
section => 'timesavers',
about => 'Find the change that introduced a bug by binary search'
},
grep => {
unmodified_help => 1,
unmodified_behavior => 1,
extra => 1,
section => 'discovery',
about => 'Print lines of known files matching a pattern'
},
mv => {
unmodified_help => 1,
unmodified_behavior => 1,
section => 'modification',
about => 'Move or rename files (or directories or symlinks)'
},
);
}
#*************************************************************************#
#*************************************************************************#
#*************************************************************************#
# CLASSES DEFINING ACTIONS TO PERFORM #
#*************************************************************************#
#*************************************************************************#
#*************************************************************************#
###########################################################################
# subcommand, a base class for all eg subcommands #
###########################################################################
package subcommand;
sub new {
my $class = shift;
my $self = {git_repo_needed => 0, @_}; # Hashref initialized as we're told
bless($self, $class);
# Our "see also" section in help usually references the same subsection
# as our class name.
$self->{git_equivalent} = ref($self) if !defined $self->{git_equivalent};
# We allow direct instantiation of the subcommand class only if they
# provide a command name for us to pass to git.
if (ref($class) eq "subcommand" && !defined $self->{command}) {
die "Invalid subcommand usage"
}
# Most commands must be run inside a git working directory
unless (!$self->{git_repo_needed} || (@ARGV > 0 && $ARGV[0] eq "--help")) {
$self->{git_dir} = RepoUtil::git_dir();
if (!defined $self->{git_dir}) {
# Couldn't find git repository. That could be for any of three reasons:
# 1) Is 'git' even in the user's PATH?
my @paths_to_git = grep {-x "$_/git"} split(/:/, $ENV{PATH});
if (!@paths_to_git) {
die "Error: Cannot find git in your PATH!\n";
}
# 2) Does 'git' even work (possible bad installation)?
my ($ret, $output) =
ExecUtil::execute_captured("$GIT_CMD --version", ignore_ret => 1);
if ($ret != 0 || $output !~ /^git version .*$/) {
die "Error: Cannot execute git (check your git installation)!\n";
}
# 3) They just aren't in a project tracked by git
die "Error: Must be run inside a git-tracked working directory!\n";
}
}
# Many commands do not work if no commit has yet been made
if ($self->{initial_commit_error_msg} &&
RepoUtil::initial_commit() &&
(@ARGV < 1 || $ARGV[0] ne "--help")) {
die "$self->{initial_commit_error_msg}\n";
}
return $self;
}
sub help {
my $self = shift;
my $package_name = ref($self);
$package_name =~ s/_/-/; # Packages use underscores, commands use dashes
my $git_equiv = $self->{git_equivalent};
$git_equiv =~ s/_/-/; # Packages use underscores, commands use dashes
if ($package_name eq "subcommand") {
exit ExecUtil::execute("$GIT_CMD $self->{command} --help")
}
$ENV{"LESS"} = "FRSX" unless defined $ENV{"LESS"};
my $less = ($USE_PAGER == 1) ? "less" :
($USE_PAGER == 0) ? "cat" :
`$GIT_CMD config core.pager` || "less";
chomp($less);
open(OUTPUT, "| $less");
print OUTPUT "$package_name: $COMMAND{$package_name}{about}\n";
print OUTPUT $self->{'help'};
print OUTPUT "\nDifferences from git $package_name:";
print OUTPUT "\n None.\n" if !defined $self->{'differences'};
print OUTPUT $self->{'differences'} if defined $self->{'differences'};
if ($git_equiv) {
print OUTPUT "\nSee also\n";
print OUTPUT <<EOF;
Run 'git help $git_equiv' for a comprehensive list of options available.
eg $package_name is designed to accept the same options as git $git_equiv, and
with the same meanings unless specified otherwise in the above
"Differences" section.
EOF
}
close(OUTPUT);
exit 0;
}
sub preprocess {
my $self = shift;
return if (scalar(@ARGV) > 0 && $ARGV[0] eq "--");
my $result=main::GetOptions("--help" => sub { $self->help() });
}
sub run {
my $self = shift;
my $package_name = ref($self);
my $subcommand =
$package_name eq "subcommand" ? $self->{'command'} : $package_name;
@ARGV = Util::quote_args(@ARGV);
return ExecUtil::execute("$GIT_CMD $subcommand @ARGV", ignore_ret => 1);
}
###########################################################################
# add #
###########################################################################
package add;
@add::ISA = qw(subcommand);
INIT {
$COMMAND{add} = {
unmodified_behavior => 1,
section => 'compatibility',
about => 'Mark content in files as being ready for commit'
};
}
sub new {
my $class = shift;
my $self = $class->SUPER::new(git_repo_needed => 1, @_);
bless($self, $class);
$self->{'help'} = "
Description:
eg add is provided for backward compatibility; it has identical usage and
functionality as 'eg stage'. See 'eg help stage' for more details.
";
return $self;
}
###########################################################################
# apply #
###########################################################################
package apply;
@apply::ISA = qw(subcommand);
INIT {
$COMMAND{apply} = {
about => 'Apply a patch in a git repository'
};
}
sub new {
my $class = shift;
my $self = $class->SUPER::new(git_repo_needed => 0, @_);
bless($self, $class);
$self->{'help'} = "
Usage:
eg apply [--staged] [-R | --reverse] [-pNUM]
Description:
Applies a patch to a git repository.
Examples:
Reverse changes in foo.patch
\$ eg apply -R foo.patch
(Advanced) Reverse changes since the last commit to the version of foo.c
in the staging area (equivalent to 'eg revert --staged foo.c'):
\$ eg diff --staged foo.c | eg apply -R --staged
Options:
--staged
Apply the patch to the staged (explicitly marked as ready to be committed)
versions of files
--reverse, -R
Apply the patch in reverse.
-pNUM
Remove NUM leading paths from filenames. For example, with the filename
/home/user/bla/foo.c
using -p0 would leave the name unmodified, using -p1 would yield
home/user/bla/foo.c
and using -p3 would yield
bla/foo.c
";
$self->{'differences'} = '
eg apply is identical to git apply except that it accepts --staged as a
synonym for --cached.
';
return $self;
}
sub preprocess {
my $self = shift;
my $result = main::GetOptions("--help" => sub { $self->help() });
foreach my $i (0..$#ARGV) {
$ARGV[$i] = "--cached" if $ARGV[$i] eq "--staged";
}
}
###########################################################################
# branch #
###########################################################################
package branch;
@branch::ISA = qw(subcommand);
INIT {
$COMMAND{branch} = {
section => 'projects',
about => 'List, create, or delete branches'
};
$ALIAS{'br'} = "branch";
}
sub new {
my $class = shift;
my $self = $class->SUPER::new(git_repo_needed => 1, @_);
bless($self, $class);
$self->{'help'} = "
Usage:
eg branch [-r]
eg branch [-s] NEWBRANCH [STARTPOINT]
eg branch -d BRANCH
Description:
List the existing branches that you can switch to, create a new branch,
or delete an existing branch. For switching the working copy to a
different branch, use the eg switch command instead.
Note that branches are local; creation of branches in a remote repository
can be accomplished by first creating a local branch and then pushing the
new branch to the remote repository using eg push.
Examples
List the available local branches
\$ eg branch
Create a new branch named random_stuff, based off the last commit.
\$ eg branch random_stuff
Create a new branch named sec-48 based off the 4.8 branch
\$ eg branch sec-48 4.8
Delete the branch named bling
\$ eg branch -d bling
Create a new branch named my_fixes in the default remote repository
\$ eg branch my_fixes
\$ eg push --branch my_fixes
(Advanced) Create a new branch named bling, based off the remote tracking
branch of the same name
\$ eg branch bling origin/bling
See 'eg remote' for more details about setting up named remotes and
remote tracking branches, and 'eg help topic storage' for more details on
differences between branches and remote tracking branches.
Options:
-d
Delete specified branch
-r
List remote tracking branches (see 'eg help topic storage') for more
details. This is useful when using named remote repositories (see 'eg
help remote')
-s
After creating the new branch, switch to it
";
$self->{'differences'} = '
eg branch is identical to git branch other than adding a new -s option for
switching to a branch immediately after creating it.
';
return $self;
}
sub run {
my $self = shift;
my $switch = 0;
if (scalar(@ARGV) > 1 && $ARGV[0] eq "-s") {
$switch = 1;
shift @ARGV;
}
@ARGV = Util::quote_args(@ARGV);
my $ret = ExecUtil::execute("$GIT_CMD branch @ARGV", ignore_ret => 1);
$ret = ExecUtil::execute("$GIT_CMD checkout $ARGV[0]", ignore_ret => 1)
if ($switch && $ret == 0);
return $ret;
}
###########################################################################
# bundle #
###########################################################################
package bundle;
@bundle::ISA = qw(subcommand);
INIT {
$COMMAND{bundle} = {
extra => 1,
section => 'collaboration',
about => 'Pack repository updates (or whole repository) into a file'
};
}
sub new {
my $class = shift;
my $self = $class->SUPER::new(
git_repo_needed => 1,
initial_commit_error_msg => "No bundles can be created until a commit " .
"has been made.",
@_
);
bless($self, $class);
$self->{'help'} = "
Usage:
eg bundle create FILENAME [REFERENCES]
eg bundle create-update NEWFILENAME OLDFILENAME [REFERENCES]
eg bundle verify FILENAME
Description:
Bundle creates a file which contains a repository, or a subset thereof.
This is useful when two machines cannot be directly connected (thus
preventing use of the standard interactive git protocols -- git, ssh,
rsync or http), but changes still need to be communicated between the
machines.
The remote side can use the resulting file (or the path to it) as the URL
for the repository they want to clone or pull updates from.
Examples
Create a bundle in the file repo.bundle which contains the whole repository
\$ eg bundle create repo.bundle
After getting the bundle named repo.bundle from a collaborator (which
must contain \"HEAD\" as one of the references if you explicitly list which
ones to be included at creation time), clone the repository into the
directory named project-name
\$ eg clone /path/to/repo.bundle project-name
Create a bundle in the file called new-repo containing only updates since
the bundle old-repo was created.
\$ eg bundle create-update new-repo old-repo
Pulls updates from a new bundle we have been sent.
\$ eg pull /path/to/repo.bundle
Pull updates from a new bundle we have been sent, if we first overwrite
the bundle we originally original cloned from with the new bundle
\$ eg pull
(Advanced) Create a bundle containing the two branches debug and
installer, and the tag named v2.3, in the file called my-changes
\$ eg bundle create my-changes debug installer v2.3
(Advanced) Create a bundle in the file called new-repo that contains
updates since the bundle old-bundle was created, but don't include the
new branch secret-stuff or crazy-idea
\$ eg bundle create-update new-repo old-bundle ^secret-stuff ^crazy-idea
Options:
eg bundle create FILENAME [REFERENCES]
eg bundle create-update NEWFILENAME OLDFILENAME [REFERENCES]
eg bundle verify FILENAME
create FILENAME [REFERENCES]
Create a new bundle in the file FILENAME. If no REFERENCES are passed,
all branches and tags (plus \"HEAD\") will be included. See below for
a basic explanation of REFERENCES.
create-update NEWFILENAME OLDFILENAME [REFERENCES]
Create a new bundle in the file NEWFILENAME, but don't include any
commits already included in OLDFILENAME. See below for a basic
explanation of REFERNCES. By default, any new branch or tags will be
included as well; exclude specific branches or tags by passing ^BRANCH
or ^TAG as a reference; see below for more details.
verify FILENAME
Check whether the given bundle in FILENAME will cleanly apply to the
current repository.
REFERENCES
Which commits to include or exclude from the bundle. Probably best
explained by example:
Example Meaning
----------------- --------------------------------------------------
master Include the master branch
master~10..master Include the last 10 commits on the master branch
^baz foo bar Include commits on the foo or bar branch, except for
those that are in the baz branch
";
$self->{'differences'} = '
eg bundle differs from git bundle in two ways:
(1) eg bundle defaults to "--all HEAD" if no revisions are passed to create
(2) eg bundle provides a create-update subcommand
';
return $self;
}
sub preprocess {
my $self = shift;
#
# Parse options
#
my @args;
my $result=main::GetOptions("--help" => sub { $self->help() });
# Get the (sub)subcommand
$self->{subcommand} = shift @ARGV;
push(@args, $self->{subcommand});
if ($self->{subcommand} eq 'create') {
my $filename = shift @ARGV ||
die "Error: need a filename to write bundle to.\n";
push(@args, $filename); # Handle the filename
if (!@ARGV) {
push(@args, ("--all", "HEAD"));
}
}
elsif ($self->{subcommand} eq 'create-update') {
pop(@args); # 'create-update' isn't a real git bundle subcommand
my $newname = shift @ARGV ||
die "You must specify a new and an old filename.\n";
my $oldname = shift @ARGV ||
die "You must also specify an old filename\n";
die "$oldname does not exist.\n" if ! -f $oldname;
my ($retval, $output) =
ExecUtil::execute_captured("$GIT_CMD bundle list-heads $oldname");
my @lines = split '\n', $output;
my @refs = map { m#^([0-9a-f]+)# && "^$1" } @lines;
push(@args, ('create', $newname, "--all", "HEAD", @refs));
}
push(@args, @ARGV);
# Reset @ARGV with the built up list of arguments
@ARGV = @args;
}
###########################################################################
# cat #
###########################################################################
package cat;
@cat::ISA = qw(subcommand);
INIT {
$COMMAND{cat} = {
new_command => 1,
extra => 1,
section => 'compatibility',
about => 'Output the current or specified version of files'
};
}
sub new {
my $class = shift;
my $self = $class->SUPER::new(
git_repo_needed => 1,
git_equivalent => 'show',
initial_commit_error_msg => "Error: Cannot show committed versions of " .
"files when no commits have occurred.",
@_
);
bless($self, $class);
$self->{'help'} = "
Usage:
eg cat [REVISION:]FILE...
Description:
Output the specified file(s) as of the given revisions.
Note that this basically is just a compatibility alias provided for users
of other SCMs. You should consider using 'git show' instead, though with
core git whenever you specify a REVISION, you will need to specify the
path to FILE relative to the toplevel project directory, instead of a
path for FILE relative to the current directory.
Examples
Output the most recently committed version of foo.c
\$ eg cat foo.c
Output the version of bar.h from the 5th to last commit on the
ugly_fixes branch
\$ eg cat ugly_fixes~5:bar.h
Concatenate the version of hello.c from two commits ago and the
version of world.h from the branch timbuktu, and output the result:
\$ eg cat HEAD~1:hello.c timbuktu:world.h
";
$self->{'differences'} = '
The output of "git show FILE" is probably confusing to users at first,
as is the need to specify files relative to the top of the git project
rather than relative to the current directory. Thus, "eg cat FILE"
calls "git show HEAD:PATH/TO/FILE".
';
return $self;
}
sub preprocess {
my $self = shift;
my $result=main::GetOptions("--help" => sub { $self->help() });
# Get important directories
my ($cur_dir, $top_dir, $git_dir) = RepoUtil::get_dirs();
my @args;
foreach my $arg (@ARGV) {
if ($arg !~ /:/) {
my ($path) = Util::reroot_paths__from_to_files($cur_dir, $top_dir, $arg);
push(@args, "HEAD:$path");
} else {
my ($REVISION, $FILE) = split(/:/, $arg, 2);
my ($path) = Util::reroot_paths__from_to_files($cur_dir, $top_dir, $FILE);
push(@args, "$REVISION:$path");
}
}
@ARGV = @args;
}
sub run {
my $self = shift;
@ARGV = Util::quote_args(@ARGV);
return ExecUtil::execute("$GIT_CMD show @ARGV", ignore_ret => 1);
}
###########################################################################
# changes #
###########################################################################
package changes;
@changes::ISA = qw(subcommand);
INIT {
$COMMAND{changes} = {
new_command => 1,
section => 'misc',
about => 'Provide an overview of the changes from git to eg'
};
}
sub new {
my $class = shift;
my $self = $class->SUPER::new(git_repo_needed => 1, git_equivalent => '', @_);
bless($self, $class);
$self->{'help'} = "
Usage:
eg changes [--details]
Options
--details
In addition to the summary of which commands were changed, list the
changes to each command.
";
$self->{'differences'} = '
eg changes is unique to eg; git does not have a similar command.
';
return $self;
}
sub preprocess {
my $self = shift;
$self->{details} = 0;
my $result = main::GetOptions(
"--help" => sub { $self->help() },
"--details" => \$self->{details},
);
die "Unrecognized arguments: @ARGV\n" if @ARGV;
}
sub run {
my $self = shift;
if ($DEBUG == 2) {
print " >>(No commands to run, just data to print)<<\n";
return;
}
# Print valid subcommands sorted by section
my $indent = " ";
my $header_indent = "";
$ENV{"LESS"} = "FRSX" unless defined $ENV{"LESS"};
my $less = ($USE_PAGER == 1) ? "less" :
($USE_PAGER == 0) ? "cat" :
`$GIT_CMD config core.pager` || "less";
chomp($less);
open(OUTPUT, "| $less");
if ($self->{details}) {
print OUTPUT "Summary of changes:\n";
$indent = " ";
$header_indent = " ";
}
print OUTPUT "${header_indent}Modified Behavior:\n";
foreach my $c (sort keys %COMMAND) {
next if $COMMAND{$c}{unmodified_behavior};
next if $COMMAND{$c}{new_command};
print OUTPUT "$indent$c\n";
}
print OUTPUT "${header_indent}New commands:\n";
foreach my $c (sort keys %COMMAND) {
next if !$COMMAND{$c}{new_command};
print OUTPUT "$indent$c\n";
}
print OUTPUT "${header_indent}Modified Help Only:\n";
foreach my $c (sort keys %COMMAND) {
next if $COMMAND{$c}{unmodified_help};
next if !$COMMAND{$c}{unmodified_behavior};
next if $COMMAND{$c}{new_command};
print OUTPUT "$indent$c\n";
}
if ($self->{details}) {
foreach my $c (sort keys %COMMAND) {
next if $COMMAND{$c}{unmodified_help} || $COMMAND{$c}{unmodified_behavior};
my $real_c = $c;
$c =~ s/-/_/; # Packages use underscores, commands use dashes
next if !$c->can("new");
my $obj = $c->new(initial_commit_error_msg => '');
print OUTPUT "Changes to $real_c:\n";
if ($obj->{differences}) {
$obj->{differences} =~ s/^\n//;
print OUTPUT $obj->{differences};
} else {
print OUTPUT " <Unknown>.\n";
}
}
}
close(OUTPUT);
}
###########################################################################
# checkout #
###########################################################################
package checkout;
@checkout::ISA = qw(subcommand);
INIT {
$COMMAND{checkout} = {
section => 'compatibility',
about => 'Compatibility wrapper for clone/switch/revert'
};
}
sub new {
my $class = shift;
my $self = $class->SUPER::new(git_repo_needed => 0, @_);
bless($self, $class);
$self->{'help'} = "
Usage:
eg checkout [-b] BRANCH
eg checkout [REVISION] PATH...
Description:
eg checkout mostly exists as a compatibility wrapper for those used to
other systems (cvs/svn and git). If you:
(1) want a new copy of the source code from a remote repository
OR
(2) want to switch your working copy to a different branch
OR
(3) want to revert the contents of a file to its content from a
different revision
Then use:
(1) eg clone
(2) eg switch
(3) eg revert
eg checkout will accept the same arguments as eg clone (for getting a new
copy of the source code from a remote repository), but will provide an
error message and tell the user to use eg clone in such cases.
The first usage form of eg checkout is used to switch to a different
branch (optionally also creating it first). This is something that can
be done with no network connectivity in git and thus eg. Users can find
identical functionality in eg switch.
The second usage form of eg checkout is used to replace files in the
working copy with versions from an older commit, i.e. to revert files to
an older version. Note that this only works when the specified files
also existed in the older version (eg checkout will not delete or unstage
files for you), does not work for the initial commit (since there's no
older revision to revert back to -- unless you are an advanced user
interested in just undoing the changes since the most recent staging),
and cannot be used to undo an incomplete merge (since it only operates on
a subset of files and not everything since a given commit). Users can
find the same functionality (without all the caveats) as well as other
capabilities in eg revert.
Examples:
Switch to the stable branch
\$ eg checkout stable
Replace foo.c with the third to last version before the most recent
commit (Note that HEAD always refers to the current branch, and the
current branch always refers to its most recent commit)
\$ eg checkout HEAD~3 foo.c
";
$self->{'differences'} = '
eg checkout accepts all parameters that git checkout accepts with the
same meanings and same output (eg checkout merely calls git checkout in
such cases).
The only difference between eg and git regarding checkout is that eg
checkout will also accept all arguments to git clone, and then tell users
that they must have meant to run eg clone (a much nicer error message for
users trying to get a copy of source code from a remote repository than
"fatal: Not a git repository").
';
return $self;
}
sub _looks_like_git_repo ($) {
my $path = shift;
my $clone_protocol = qr#^(?:git|ssh|http|https|rsync)://#;
my $git_dir = RepoUtil::git_dir();
my $in_working_copy = defined $git_dir ? 1 : 0;
# If the path looks like a git, ssh, http, https, or rsync url, then it
# looks like we're being given a url to a git repo
if ($path =~ /$clone_protocol/) {
return 1;
}
# If the path isn't a clone_protocol url and isn't a directory, it can't be
# a git repo
if (! -d $path) {
return 0;
}
my $path_is_absolute = ($path =~ m#^/#);
return (!$in_working_copy || ($in_working_copy && $path_is_absolute));
}
sub preprocess {
my $self = shift;
if (scalar(@ARGV) > 0 && $ARGV[0] ne "--") {
main::GetOptions("--help" => sub { $self->help() });
}
$self->{command} = 'checkout';
die "eg checkout requires at least one argument.\n" if !@ARGV;
#
# Determine whether this should be a call to git clone or git checkout
#
my $clone_protocol = qr#^(?:git|ssh|http|https|rsync)://#;
if (_looks_like_git_repo($ARGV[-1]) ||
(! -d $ARGV[-1] && @ARGV > 1 && _looks_like_git_repo($ARGV[-2]))
) {
$self->{command} = 'clone';
}
}
sub run {
my $self = shift;
@ARGV = Util::quote_args(@ARGV);
if ($self->{command} ne 'clone') {
# If this operation isn't a clone, then we should have checked for
# whether we are in a git directory. But we didn't do that, just in
# case it was a clone. So, do it now.
$self->{git_dir} = RepoUtil::git_dir();
die "Must be run inside a git repository!\n" if !defined $self->{git_dir};
return ExecUtil::execute("$GIT_CMD checkout @ARGV", ignore_ret => 1);
} else {
die "Did you mean to run\n eg clone @ARGV\n?\n";
}
}
###########################################################################
# cherry_pick #
###########################################################################
package cherry_pick;
@cherry_pick::ISA = qw(subcommand);
INIT {
$COMMAND{"cherry-pick"} = {
extra => 1,
section => 'modification',
about => 'Apply (or reverse) a commit, usually from another branch'
};
}
sub new {
my $class = shift;
my $self = $class->SUPER::new(git_repo_needed => 1, @_);
bless($self, $class);
$self->{'help'} = "
Usage:
eg cherry-pick [--reverse] [--edit] [-n] [-m parent-number] [-s] [-x] REVISION
Description:
Given an existing revision, apply the change between its parent and it
(or reverse apply if the --reverse option is present), and record a new
revision with this change. Your working tree must be clean (no local
unsaved modifications) in order to run eg cherry-pick.
Examples:
Apply the fix 3 commits behind the tip of the experimental branch
(i.e. the fix made in experimental~3) to the current branch
\$ eg cherry-pick experimental~3
Make a new commit that reverses the changes made in the most recent
commit of the current branch
\$ eg cherry-pick -R HEAD
Options:
--reverse, --revert, -R
Reverse apply the changes from the specified commit (i.e. revert the
specified revision with a new commit).
--edit, -e
With this option, eg cherry-pick will let you edit the commit message
prior to committing.
-x
When recording the commit, append to the original commit message a note
that indicates which commit this change was cherry-picked from. Append
the note only for cherry picks without conflicts. Do not use this
option if you are cherry-picking from your private branch because the
information is useless to the recipient. If on the other hand you are
cherry-picking between two publicly visible branches (e.g. backporting
a fix to a maintenance branch for an older release from a development
branch), adding this information can be useful.
This option is turned on automatically when -R is specified.
--mainline parent-number, -m parent-number
cherry-pick always applies the changes between a revision and its
parent; thus if a revision represents a merge commit, it is not clear
which parent cherry-pick should get the changes relative to. This
option specifies the parent number (starting from 1) of the mainline
and allows cherry-pick to replay the change relative to the specified
parent.
--no-commit, -n
Usually cherry-pick automatically creates a commit. This flag applies
the change necessary to cherry-pick the named revision to your working
tree and staging area, but does not make the commit. In addition, when
this option is used, the staging area can contain unsaved changes and
the cherry-pick will be done against the beginning state of your
staging area.
This is useful when cherry-picking more than one commit into a single
combined change.
--signoff, -s
Add Signed-off-by line at the end of the commit message.
REVISION
A reference to a recorded version of the repository. See 'eg help
topic revisions' for more details.
";
$self->{'differences'} = '
eg cherry-pick contains both the functionality of git cherry-pick and git
revert. If the -R option is specified, eg cherry-pick calls git revert
(after removing the -R option); otherwise it calls git cherry-pick.
';
return $self;
}
sub preprocess {
my $self = shift;
my ($reverse, $dash_x, $mainline) = (0, 0, -1);
Getopt::Long::Configure("permute"); # Allow unrecognized options through
my $result = main::GetOptions(
"--help" => sub { $self->help() },
"mainline|m=i" => \$mainline,
"reverse|R" => \$reverse,
"revert" => \$reverse,
"x" => \$dash_x,
);
$self->{reverse} = $reverse;
unshift(@ARGV, "-x") if (!$reverse && $dash_x);
unshift(@ARGV, ("-m", $mainline)) if $mainline != -1;
}
sub run {
my $self = shift;
@ARGV = Util::quote_args(@ARGV);