File Coverage

blib/lib/TheOneRing.pm
Criterion Covered Total %
statement 7 9 77.7
branch n/a
condition n/a
subroutine 3 3 100.0
pod n/a
total 10 12 83.3


line stmt bran cond sub pod time code
1             # Copyright (C) 2009 Wes Hardaker
2             # License: GNU GPLv2. See the COPYING file for details.
3             package TheOneRing;
4              
5 4     4   165738 use strict;
  4         10  
  4         284  
6 4     4   5104 use UNIVERSAL;
  4         61  
  4         28  
7 4     4   1870 use Getopt::GUI::Long;
  0            
  0            
8              
9             our $VERSION = '0.3';
10              
11             our %master_arguments =
12             (
13             'commit' =>
14             [
15             ["m|message|msg=s" => "Commit message"],
16             ["N|non-recursive" => "Don't decend into subdirectiories"],
17             ["q|quiet" => "Update quietly as possible"],
18             ],
19              
20             'diff' =>
21             [
22             ["r|revision|msg=s" => "Revision to diff against"],
23             ["N|non-recursive" => "Don't decend into subdirectiories"],
24             ],
25              
26             'update' =>
27             [
28             ["r|revision=s" => "Revision to update to"],
29             ["N|non-recursive" => "Don't decend into subdirectiories"],
30             ["q|quiet" => "Update quietly as possible"],
31             ],
32              
33             'annotate' =>
34             [
35             ["r|revision=s" => "Revision to update to"],
36             ["N|non-recursive" => "Don't decend into subdirectiories"],
37             ],
38              
39             'status' =>
40             [
41             ["q|quiet" => "Quiet output"],
42             ],
43              
44             'info' =>
45             [
46             ["q|quiet" => "Quiet output"],
47             ],
48              
49             'add' =>
50             [
51             ["N|non-recursive" => "Don't decend into subdirectiories"],
52             ["q|quiet" => "Quiet output"],
53             ],
54              
55             'remove' =>
56             [
57             ["N|non-recursive" => "Don't decend into subdirectiories"],
58             ["q|quiet" => "Quiet output"],
59             ],
60              
61             'list' =>
62             [
63             ["r|revision=s" => "Revision to update to"],
64             ["N|non-recursive" => "Don't decend into subdirectiories"],
65             ["q|quiet" => "Quiet output"],
66             ],
67              
68             'export' =>
69             [
70             ["r|revision=s" => "Revision to update to"],
71             ["N|non-recursive" => "Don't decend into subdirectiories"],
72             ["q|quiet" => "Quiet output"],
73             ],
74              
75             'log' =>
76             [
77             ["r|revision=s" => "Revision to update to"],
78             ["N|non-recursive" => "Don't decend into subdirectiories"],
79             ["q|quiet" => "Quiet output"],
80             ],
81              
82             'revert' =>
83             [
84             ["N|non-recursive" => "Don't decend into subdirectiories"],
85             ["q|quiet" => "Quiet output"],
86             ],
87              
88             'move' =>
89             [
90             ],
91              
92             'ignore' =>
93             [
94             ],
95              
96             # XXX: multiple things offer recursive
97             # (need a way to specify that -N means *don't* do -R or something
98              
99             #XXX: log
100             #XXX: push
101             #XXX: pull
102             #XXX: tag
103             #XXX: move
104             #XXX: mkdir
105             #XXX: cat
106             #XXX: resolve(d)
107             #XXX: import?
108             #XXX: lock? / unlock
109             #XXX: create?
110             #XXX: property sets
111             #XXX: switch
112              
113             );
114              
115             # XXX: any benifit to making this per-object? local overrides by object user?
116             our %aliases =
117             (
118             blame => 'annotate',
119             ann => 'annotate',
120              
121             co => 'checkout',
122              
123             ci => 'commit',
124              
125             di => 'diff',
126              
127             ls => 'list',
128            
129             st => 'status',
130             stat => 'status',
131              
132             up => 'update',
133             );
134              
135              
136             # note: this new clause is used by most sub-modules too, altering it
137             # will alter them.
138             sub new {
139             my $type = shift;
140             my ($class) = ref($type) || $type;
141             my $self = {};
142             $self->{'options'} = {@_};
143             bless($self, $class);
144             $self->init();
145             return $self;
146             }
147              
148             # prototype for children to optionally override
149             sub init {
150             }
151              
152             # the meat of the work
153             sub dispatch {
154             my ($self, $command, @args) = @_;
155              
156             my $repotype;
157              
158             if (exists($aliases{$command})) {
159             $command = $aliases{$command};
160             }
161              
162             # do checkout/stuff first
163             if ($command eq 'checkout' || $command eq 'co' ||
164             $command eq 'export') {
165             # XXX
166             return 1;
167             }
168              
169             #
170             # we could do a bunch of autoloading tricks to have each module
171             # self-identify, but that would be a lot slower. By only loading
172             # the module we need and hard coding this determination list it
173             # should make the one ring a bit faster to run.
174             #
175              
176             # determine based on what's in the directory
177             if (-d '.svn') {
178             # that's an easy check.
179             $repotype = 'SVN';
180             } elsif (-d 'CVS') {
181             # that's an easy check.
182             $repotype = 'CVS';
183             } elsif (-d '.git' || -d '../.git' || -d '../../.git'
184             || -d '../../../.git' || -d '../../../../.git'
185             || -d '../../../../.git' || -d '../../../../../.git') {
186             # that's an easy check.
187             # XXX: yeah, that'll scale... needs to recursively go up
188             $repotype = 'GIT';
189             } else {
190             $repotype = $self->find_cached_type();
191             if (!defined($repotype)) {
192             # XXX: try dynamic system of some kind here now that the speed
193             # attempt is done?
194             $self->ERROR("Failed to determine the type of repository we're in.");
195             }
196             }
197             $self->debug("found subtype $repotype");
198              
199             my $submodule = $self->load_subtype($repotype);
200             $self->ERROR("failed to load $repotype: \n", $@) if (!$submodule);
201              
202             $self->debug("running $repotype->$command");
203              
204             # they have a method defined
205             if ($submodule->can($command)) {
206             $submodule->$command(@args);
207             return 1;
208             }
209              
210             # see if they have a defined command mapping
211             if (exists($submodule->{'mapping'}{$command})) {
212             # process and run it
213             $submodule->map_and_run($command,
214             $submodule->{'mapping'}{$command},
215             @args);
216             return 1;
217             }
218              
219             $self->ERROR("ERROR: The \"$repotype\" module does know the command \"$command\"");
220             }
221              
222             sub expect_string {
223             my ($self, $value, @args) = @_;
224             if (!defined($value)) {
225             return $value; #XXX: should be an error?
226             }
227             if (ref($value) eq 'CODE') {
228             return $value->($self, @args);
229             } elsif ($value eq 'ARRAY' && ref($value->[0]) eq 'CODE') {
230             my $code = shift @$value;
231             return $code->($self, @$value, @args);
232             } elsif ($value eq 'ARRAY') {
233             $self->ERROR("Expected a generic STRING and got an ARRAY");
234             } elsif ($value eq 'HASH') {
235             $self->ERROR("Expected a generic STRING and got a HASH");
236             }
237             return $value;
238             }
239              
240             sub expect_array {
241             my ($self, $value, @args) = @_;
242             if (ref($value) eq 'ARRAY' && ref($value->[0]) ne 'CODE') {
243             return $value;
244             } elsif (ref($value) eq 'ARRAY' && ref($value->[0]) eq 'CODE') {
245             my $code = shift @$value;
246             my $result = $code->($self, @$value, @args);
247             return $result if (ref($result) eq 'ARRAY');
248             return [$result];
249             } elsif (ref($value) eq 'ARRAY') {
250             return $value;
251             } elsif (ref($value) eq 'CODE') {
252             my $result = $value->($self, @args);
253             return $result if (ref($result) eq 'ARRAY');
254             return [$result];
255             }
256              
257             return [$self->expect_string($value, @args)];
258             }
259              
260             sub save_ARGV {
261             my ($self, $newargv, @newargs) = @_;
262              
263             # save the current program name
264             $self->{'savedprog'} = $main::0;
265             $main::0 = $newargv if (defined($newargv));
266              
267             # save the existing ARGV arguments (just in case)
268             @{$self->{'savedARGV'}} = @main::ARGV;
269             @main::ARGV = @newargs;
270             }
271              
272             sub restore_ARGV {
273             my ($self) = @_;
274             @main::ARGV = @{$self->{'savedARGV'}} if (defined($self->{'savedARGV'}));
275             $main::0 = $self->{'savedprog'} if (defined($self->{'savedprog'}));
276             delete $self->{'savedprog'};
277             }
278              
279              
280             sub map_args {
281             my ($self, $subcmd, $map, @args) = @_;
282             my %opts = @{$self->{$subcmd}{'defaults'} || []};
283              
284             # first process against the known arguments
285             my $cmdoptions = $master_arguments{$subcmd};
286             unshift @$cmdoptions, ["GUI:otherargs_text", " "];
287              
288             # though it's discouraged to add more on a per-submodule, we do support it.
289             push @$cmdoptions, @{$self->{'master_arguments'}}
290             if (exists($self->{'master_arguments'}));
291              
292             Getopt::GUI::Long::Configure(qw(display_help no_ignore_case no_gui
293             require_order allow_zero));
294              
295             # save the current program name
296             $self->save_ARGV("$main::0 [OR OPTIONS] $subcmd", @args);
297              
298             # and process our local arguments, and return everything to normal
299             GetOptions(\%opts, @$cmdoptions) || exit;
300              
301             my @remainingargs = @main::ARGV;
302              
303             # restore the saved args
304             $self->restore_ARGV();
305              
306             # process %opts
307              
308             my $newcommand = $self->expect_string($self->{'command'}, @remainingargs);
309             my $newsubcmd =
310             $self->expect_string($map->{'command'}, @remainingargs) || $subcmd;
311              
312             my $argsmap = $map->{'args'};
313              
314             # build a list of options for the called command based on the
315             # options for our command.
316             my @options;
317             @options = (@{$self->expect_array($map->{'options'}, @remainingargs)})
318             if (exists($map->{'options'}));
319             foreach my $optkey (keys(%opts)) {
320             if (!exists($argsmap->{$optkey})) {
321             $self->ERROR("\"$newcommand $newsubcmd\" does not support the -$optkey option");
322             }
323             if ($argsmap->{$optkey} =~ /^-/) {
324             # argument with a value indicated by a leading -
325             push @options, "$argsmap->{$optkey}", $opts{$optkey};
326             } else {
327             # singular argument
328             push @options, "-$argsmap->{$optkey}";
329             }
330             }
331             return ($newcommand, $newsubcmd, \@options, \@remainingargs, \%opts);
332             }
333              
334             sub map_and_run {
335             my ($self, $subcmd, $map, @args) = @_;
336              
337             my ($cmd, $options, $otherargs);
338             ($cmd, $subcmd, $options, $otherargs) =
339             $self->map_args($subcmd, $map, @args);
340              
341             $self->System($cmd, $subcmd, @$options, @$otherargs);
342             }
343              
344             sub load_subtype {
345             my ($self, $type) = @_;
346              
347             # try and load it
348             my $havesubmod = eval "require TheOneRing::$type;";
349             return if (!$havesubmod);
350              
351             # once loaded, create an instance
352             my $submod = eval "new TheOneRing::$type();";
353              
354             # copy in our running options
355             $submod->{'options'} = $self->{'options'};
356              
357             return $submod;
358             }
359              
360             sub get_cwd {
361             require Cwd;
362             return Cwd::getcwd();
363             }
364              
365             sub get_config_dir {
366             my ($self) = @_;
367              
368             my $ordir = $self->{'configdir'} || $ENV{'HOME'} . "/.theonering/";
369             if (! -d $ordir) {
370             mkdir($ordir);
371             }
372             return $ordir;
373             }
374              
375             sub get_config_file {
376             my ($self, $filename) = @_;
377              
378             my $dir = $self->get_config_dir();
379             return "$dir/$filename";
380             }
381              
382             sub find_cached_type {
383             my ($self, $cwd) = @_;
384              
385             $cwd ||= $self->get_cwd();
386              
387             # check the current cache if possible
388             my $type = $self->check_known_types($cwd);
389             return $type if (defined($type));
390              
391             # ok, failing that lets try and create a fresh list.
392             $self->debug("building a fresh list\n");
393             $self->build_known_types();
394              
395             # Then try again now that we have a fresh list
396             return $self->check_known_types($cwd);
397             }
398              
399             sub check_known_types {
400             my ($self, $cwd) = @_;
401              
402             $cwd ||= $self->get_cwd();
403              
404             my $typecache = $self->get_config_file('typecache');
405              
406             if (-f $typecache) {
407             open(DIRTYPES, $typecache);
408             while () {
409             chomp();
410             my ($dir, $type) = split;
411             if ($dir eq $cwd) {
412             close(DIRTYPES);
413             return $type;
414             }
415             }
416             close(DIRTYPES);
417             }
418             return; # fail!
419             }
420              
421             sub build_known_types {
422             my ($self) = @_;
423             # do some things to build the
424              
425             my $typecache = $self->get_config_file('typecache');
426              
427             my $dir = $self->get_config_dir;
428             open(DIRTYPES,">$typecache");
429              
430             # svk map the existing checkout list
431             open(SVKLIST, "svk co --list|");
432             while () {
433             last if (/==========/);
434             }
435             while () {
436             my @stuff = split();
437             printf DIRTYPES "%-60s SVK\n",$stuff[$#stuff];
438             }
439             close(SVKLIST);
440              
441             close(DIRTYPES);
442             }
443              
444             sub debug {
445             my $self = shift;
446             if ($self->{'options'}{'debug'}) {
447             print STDERR (join(" ",@_), "\n");
448             }
449             }
450              
451             sub ERROR {
452             my ($self, @args) = shift;
453             print STDERR (join(" ",@_),"\n");
454             exit 1;
455             }
456              
457             sub System {
458             my $self = shift;
459             if ($self->{'options'}{'dryrun'}) {
460             print STDERR "would run: '", join("' '",@_), "'\n";
461             } else {
462             $self->debug("running: ", join("' '",@_));
463             system(@_);
464             }
465             }
466              
467             # used by submodules for adding lines (like "ignore file") to a static file
468             sub add_to_file {
469             my ($self, $file, @params) = @_;
470             open(IGFILE,">>$file");
471             foreach my $line (@params) {
472             print IGFILE "$line\n";
473             }
474             close(IGFILE);
475             }
476              
477             #
478             # common functions
479             #
480             sub move_by_adddel {
481             my ($self, @args) = @_;
482             $self->ERROR("move can only take one OLD and one NEW file")
483             if ($#args != 1);
484              
485             my ($old, $new) = @args;
486             rename($old, $new);
487             $self->dispatch("remove", "$old");
488             $self->dispatch("add", "$new");
489             }
490              
491             #
492             # XXX common argument processing needed
493             # ideas: * have each submodule publish a hash ref of things it can accept
494             # plus a mapping table of one ring arguments to sub-command args
495             # * fail on unknown arg based on list
496             # * --something for forced arg passing
497             #
498              
499             #
500             # XXX: create an AUTOLOAD subroutine to throw an error when someone
501             # tries to run a command on a mode that doesn't exist.
502             #
503              
504             1;
505              
506             =head1 NAME
507              
508             TheOneRing - A high level perl class to bind all VCs together
509              
510             =head1 SYNOPSIS
511              
512             my $or = new TheOneRing();
513             $or->dispatch("commit", "-m", "checking in some files", "file1", "file2");
514              
515             =head1 DESCRIPTION
516              
517             B is merely a wraper around child classes that knows how
518             to pick which child class to load based on the current working
519             directory. IE, if in a CVS checkout directory then the
520             TheOneRing::CVS module is loaded and the child is called to process
521             the command.
522              
523             B is the command line wrapper around this class, and is what most
524             users are expected to need.
525              
526             =head2 Programming Child Classes
527              
528             Most commands can be processed by simple definitions without coding
529             that can be defined in the child's I function. More complex
530             conversion requirements can be done by defining a subroutine name for
531             the action desired.
532              
533             The TheOneRing::CVS module is actually a good reference module since
534             it uses both the automatic command line mapping features as well as
535             subroutines to implement it's goals.
536              
537             Yes, much more documentation is needed here.
538              
539             =head1 SEE ALSO
540              
541             The command line wrapper: or(1)
542              
543             =head1 AUTHOR
544              
545             Wes Hardaker
546              
547             =cut
548