#! /usr/bin/perl # stodo commits the changes made to a subversion backed todo db, and # is released under the terms of the GPL version 2, or any later # version, at your option. See the file README and COPYING for more # information. Copyright 2004 by Don Armstrong . # $Id$ use warnings; use strict; use Getopt::Long; use Pod::Usage; =head1 NAME stodo - devtodo event script which attempts to commit the changes made to a .todo file after a change =head1 SYNOPSIS stodo [options] Options: --simulate, -s simulate: don't actually do anything --quiet, -q don't output to STDOUT --debug, -d debugging level (Default 0) --help, -h display this help --man, -m display manual =head1 OPTIONS =over =item B<--simulate, -s> Simulation mode, don't actually commit anything. =item B<--quiet, -q> Don't output anything to STDOUT =item B<--debug, -d> Debug verbosity. (Default 0) =item B<--help, -h> Display brief useage information. =item B<--man, -m> Display this manual. =back =head1 ENVIRONMENTAL VARIABLES =over =item B Todo databse location, set by devtodo =item B If set, stodo assumes that there is no network, and doesn't commit =back =head1 EXAMPLES on save { exec stodo } =cut use Cwd qw(cwd); use IO::File; use XML::Simple; use IO::Handle; use Data::Diff qw(Diff); use File::Basename qw(dirname basename); use vars qw($DEBUG); # XXX parse config file my %options = (quiet => 0, debug => 0, help => 0, man => 0, simulate => 0, ); GetOptions(\%options,'debug|d+','help|h|?','man|m','quiet|q+','simulate|s+'); pod2usage() if $options{help}; pod2usage({verbose=>2}) if $options{man}; $DEBUG = $options{debug}; # Figure out what we're doing my $tododb = exists $ENV{TODODB} ? $ENV{TODODB} : '.todo'; if ($tododb !~ m#^/#) { $tododb = cwd().'/'.$tododb; } if (not -r $tododb) { print STDERR "Todo DB '$tododb' does not exist or is not readable\n"; exit 1; } $tododb = full_readlink($tododb); my $base_dir = dirname($tododb); my $tododb_name = basename($tododb); if (not -e "$base_dir/.svn") { print STDERR "$base_dir/.svn doesn't exist, not commiting\n" unless $options{quiet}; exit 0; } if (not -e "$base_dir/.svn/text-base/${tododb_name}.svn-base") { print STDERR "$base_dir/.svn/text-base/${tododb_name}.svn-base doesn't exist, not committing\n" unless $options{quiet}; exit 0; } # Make sure that we can ping the subversion server my $svn_entries_fh = new IO::File "$base_dir/.svn/entries",'r' or die "unable to open $base_dir/.svn/entries: $!"; my $url_type; my $svn_host; while (<$svn_entries_fh>) { next unless m#^(?:\s+url=\")?(http|svn\+ssh|file):///?([^\/]+)#; $url_type = $1; last if $url_type eq 'file'; $svn_host = $2; last; } if ($ENV{STODO_NO_COMMIT}) { print STDERR "Exiting because of STODO_NO_COMMIT env variable\n" unless $options{quiet}; exit 0; } if (not defined $svn_host and (not defined $url_type or $url_type ne 'file')) { die "Was unable to find which host the svn repository is located on"; } if ($url_type ne 'file') { qx(ping -q -c 3 $svn_host >/dev/null 2>&1); if ($? != 0) { print "Unable to ping $svn_host\n" unless $options{quiet}; exit 0 unless $options{simulate}; } } # See what has changed #my $svn_fh = new IO::Handle; #open($svn_fh,'-|','svn','diff',$tododb) or die "Unable to execute svn diff $tododb; $!"; #while (<$svn_fh>) { # next unless /^[-+]/; #} my $todo_db_old = fix_content(XMLin("$base_dir/.svn/text-base/${tododb_name}.svn-base",KeyAttr=>'time')->{note}); my $todo_db_new = fix_content(XMLin($tododb,KeyAttr=>'time')->{note}); my @commit_message; my $diff = Diff($todo_db_new,$todo_db_old); #use Data::Dumper; #print Dumper($diff); if (exists $diff->{uniq_a}) { foreach (values %{$diff->{uniq_a}}) { my ($content,$priority) = map {s/^\s*|\s*$//g; s/\n/ /g; $_;} @{$_}{qw(content priority)}; push @commit_message,qq( * Added todo: [$priority] $content); } } if (exists $diff->{uniq_b}) { foreach (values %{$diff->{uniq_b}}) { my ($content,$priority) = map {s/^\s*|\s*$//g; s/\n/ /g; $_;} @{$_}{qw(content priority)}; push @commit_message,qq( * Deleted todo: [$priority] $content); } } if (exists $diff->{diff}) { # This entry has either been edited, completed, or uncompleted foreach my $entry_key (keys %{$diff->{diff}}) { my ($content,$priority) = map {s/^\s*|\s*$//g; s/\n/ /g; $_;} @{$todo_db_new->{$entry_key}}{qw(content priority)}; my $diff_entry = $diff->{diff}{$entry_key}; if (exists $diff_entry->{uniq_a} and exists $diff_entry->{uniq_a}{done}) { push @commit_message,qq( * Finished todo: [$priority] $content); } if (exists $diff_entry->{uniq_a} and exists $diff_entry->{uniq_b}{done}) { push @commit_message,qq( * Marked as undone: [$priority] $content); } if (exists $diff_entry->{diff}) { my @what_changed; if (exists $diff_entry->{diff}{content}) { push @what_changed,'content'; } if (exists $diff_entry->{diff}{priority}) { push @what_changed,'priority'; } if (not @what_changed) { @what_changed = keys %{$diff_entry->{diff}}; } push @commit_message,qq( * Todo changed ).join(' and ',@what_changed).qq( [$priority] $content); } } } if (exists $diff->{same}) { foreach my $entry_key (keys %{$diff->{same}}) { my ($content,$priority) = map {s/^\s*|\s*$//g; s/\n/ /g; $_;} @{$todo_db_new->{$entry_key}}{qw(content priority)}; my $diff_entry = $diff->{same}{$entry_key}; if (exists $diff_entry->{uniq_a} and exists $diff_entry->{uniq_a}{done}) { push @commit_message,qq( * Finished todo: [$priority] $content); } if (exists $diff_entry->{uniq_a} and exists $diff_entry->{uniq_b}{done}) { push @commit_message,qq( * Marked as undone: [$priority] $content); } if (exists $diff_entry->{diff}) { my @what_changed; if (exists $diff_entry->{diff}{content}) { push @what_changed,'content'; } if (exists $diff_entry->{diff}{priority}) { push @what_changed,'priority'; } if (not @what_changed) { @what_changed = keys %{$diff_entry->{diff}}; } push @commit_message,qq( * Todo changed ).join(' and ',@what_changed).qq( [$priority] $content); } } } if (not @commit_message) { print "No commit message\n" if $DEBUG or $options{simulate}; exit 0; } my $commit_message = join('', map {qq($_\n)} @commit_message); if ($options{simulate}) { print "svn commit -F - $tododb <{time} => $s}; } foreach my $entry (values %{$s}) { if (exists $entry->{content}) { $entry->{content} =~ s/^\s*|\s*$//g; } if (exists $entry->{comment}) { $entry->{comment} =~ s/^\s*|\s*$//g; } } return $s; } sub full_readlink { my ($file) = @_; while (-l $file) { $file = readlink $file; } return $file; } __END__