X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/f369b8af542c644eed69bd3bf26d77dfc177c33e..af1ece782ef8ad328a97bed416a2793f9c56fc35:/mr?ds=inline

diff --git a/mr b/mr
index 175c5f1..8eb0374 100755
--- a/mr
+++ b/mr
@@ -18,6 +18,10 @@ B<mr> [options] diff
 
 B<mr> [options] log
 
+B<mr> [options] register repository
+
+B<mr> [options] config section [parameter=[value] ...]
+
 B<mr> [options] action [params ...]
 
 =head1 DESCRIPTION
@@ -74,6 +78,21 @@ Show the commit log.
 
 List the repositories that mr will act on.
 
+=item register
+
+The next parameter is the directory of an existing repository. The
+repository will be registered in the mrconfig file.
+
+=item config
+
+Modifies the mrconfig file. The next parameter is the name of the section
+to add or modify, and it is followed by one or more instances of
+"parameter=value". Use "parameter=" to remove a parameter. 
+
+For example, to add (or edit) a repository in src/foo:
+
+  mr config src/foo checkout="svn co svn://example.com/foo/trunk foo"
+
 =item help
 
 Displays this help.
@@ -84,10 +103,10 @@ Actions can be abbreviated to any unambiguous subsctring, so
 "mr st" is equivilant to "mr status", and "mr up" is equivilant to "mr
 update"
 
-Additional parameters can be passed to other commands than "commit", they
-will be passed on unchanged to the underlying revision control system.
-This is mostly useful if the repositories mr will act on all use the same
-revision control system.
+Additional parameters can be passed to most commands, and are passed on
+unchanged to the underlying revision control system. This is mostly useful
+if the repositories mr will act on all use the same revision control
+system.
 
 =head1 OPTIONS
 
@@ -112,19 +131,38 @@ Be verbose.
 =head1 FILES
 
 B<mr> is configured by .mrconfig files. It starts by reading the .mrconfig
-file in your home directory. Each repository specified in a .mrconfig file
-can also have its own .mrconfig file in its root directory that can
-optionally be used as well. So you could have a ~/.mrconfig that registers a
-repository ~/src, that itself contains a ~/src/.mrconfig file, that in turn
-registers several additional repositories.
+file in your home directory, and this can in turn chain load .mrconfig files
+from repositories.
+
+Here is an example .mrconfig file:
+
+  [src]
+  checkout = svn co svn://svn.example.com/src/trunk src
+  chain = true
+
+  [src/linux-2.6]
+  checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
 
 The .mrconfig file uses a variant of the INI file format. Lines starting with
 "#" are comments. Lines ending with "\" are continued on to the next line.
-Sections specify where each repository is located, relative to the
-directory that contains the .mrconfig file.
+
+The "DEFAULT" section allows setting default values for the sections that
+come after it.
+
+The "ALIAS" section allows adding aliases for actions. Each parameter
+is an alias, and its value is the action to use.
+
+All other sections add repositories. The section header specifies the
+directory where the repository is located. This is relative to the directory
+that contains the mrconfig file, but you can also choose to use absolute
+paths.
 
 Within a section, each parameter defines a shell command to run to handle a
-given action. Note that these shell commands are run in a "set -e" shell
+given action. mr contains default handlers for the "update", "status", and
+"commit" actions, so normally you only need to specify what to do for
+"checkout".
+
+Note that these shell commands are run in a "set -e" shell
 environment, where any additional parameters you pass are available in
 "$@". The "checkout" command is run in the parent of the repository
 directory, since the repository isn't checked out yet. All other commands
@@ -132,42 +170,36 @@ are run inside the repository, though not necessarily at the top of it.
 The "MR_REPO" environment variable is set to the path to the top of the
 repository.
 
-There are three special parameters. If the "skip" parameter is set and
-its command returns nonzero, then B<mr> will skip acting on that repository.
+A few parameters have special meanings:
+
+=over 4
+
+=item skip
+
+If the "skip" parameter is set and its command returns nonzero, then B<mr>
+will skip acting on that repository.
+
+=item chain
+
 If the "chain" parameter is set and its command returns nonzero, then B<mr>
 will try to load a .mrconfig file from the root of the repository. (You
-should avoid chaining from repositories with untrusted committers.) The
-"lib" parameter can specify some shell code that will be run before each
-command, this can be a useful way to define shell functions other commands
-can use.
-
-The "default" section allows setting up default handlers for each action,
-and is overridden by the contents of other sections. mr contains default
-handlers for the "update", "status", and "commit" actions, so normally
-you only need to specify what to do for "checkout".
+should avoid chaining from repositories with untrusted committers.)
 
-The "alias" section allows adding aliases for commands. Each parameter
-is an alias, and its value is the command to run.
+=item deleted
 
-For example:
+If the "deleted" parameter is set and its command returns nonzero, then
+B<mr> will treat the repository as deleted. It won't ever actually delete
+the repository, but it will warn if it sees the repsoitory's directory.
+This is useful when one mrconfig file is shared amoung multiple machines,
+to keep track of and remember to delete old repositories.
 
-  [src]
-  checkout = svn co svn://svn.example.com/src/trunk src
-  chain = true
+=item lib
 
-  [src/linux-2.6]
-  skip = small
-  checkout = git clone git://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux-2.6.git
+The "lib" parameter can specify some shell code that will be run before each
+command, this can be a useful way to define shell functions for other commands
+to use.
 
-  [default]
-  lib = \
-  small() {
-  	case "$(hostname)" in; \
-  	slug|snail); \
-  		return 0; ;; ; \
-  	esac; \
-  	return 1; \
-  }
+=back
 
 =head1 AUTHOR
 
@@ -193,9 +225,9 @@ my %alias;
 
 Getopt::Long::Configure("no_permute");
 my $result=GetOptions(
-	"d=s" => sub { $directory=abs_path($_[1]) },
-	"c=s" => \$config,
-	"v" => \$verbose,
+	"d|directory=s" => sub { $directory=abs_path($_[1]) },
+	"c|config=s" => \$config,
+	"verbose" => \$verbose,
 );
 if (! $result || @ARGV < 1) {
 	die("Usage: mr [-d directory] action [params ...]\n".
@@ -215,29 +247,58 @@ eval {
 
 # alias expansion and command stemming
 my $action=shift @ARGV;
+if (exists $alias{$action}) {
+	$action=$alias{$action};
+}
 if (! exists $knownactions{$action}) {
-	if (exists $alias{$action}) {
-		$action=$alias{$action};
+	my @matches = grep { /^\Q$action\E/ }
+		keys %knownactions, keys %alias;
+	if (@matches == 1) {
+		$action=$matches[0];
+	}
+	elsif (@matches == 0) {
+		die "mr: unknown action \"$action\" (known actions: ".
+			join(", ", sort keys %knownactions).")\n";
 	}
 	else {
-		my @matches = grep { /^\Q$action\E/ }
-			keys %knownactions, keys %alias;
-		if (@matches == 1) {
-			$action=$matches[0];
+		die "mr: ambiguous action \"$action\" (matches: ".
+			join(", ", @matches).")\n";
+	}
+}
+
+if ($action eq 'help') {
+	exec($config{''}{DEFAULT}{$action}) || die "exec: $!";
+}
+elsif ($action eq 'config') {
+	if (@ARGV < 2) {
+		die "mr config: not enough parameters\n";
+	}
+	my $section=shift;
+	if ($section=~/^\//) {
+		# try to convert to a path relative to $config's dir
+		my ($dir)=$config=~/^(.*\/)[^\/]+$/;
+		if ($section=~/^\Q$dir\E(.*)/) {
+			$section=$1;
 		}
-		elsif (@matches == 0) {
-			die "mr: unknown action \"$action\" (known actions: ".
-				join(", ", sort keys %knownactions).")\n";
+	}
+	my %fields;
+	foreach (@ARGV) {
+		if (/^([^=]+)=(.*)$/) {
+			$fields{$1}=$2;
 		}
 		else {
-			die "mr: ambiguous action \"$action\" (matches: ".
-				join(", ", @matches).")\n";
+			die "mr config: expected parameter=value, not \"$_\"\n";
 		}
 	}
+	modifyconfig($config, $section, %fields);
+	exit 0;
 }
-
-if ($action eq 'help') {
-	exec($config{''}{default}{help});
+elsif ($action eq 'register') {
+	my $command="set -e; ".$config{''}{DEFAULT}{lib}."\n".
+		"my_action(){ $config{''}{DEFAULT}{$action}\n }; my_action ".
+		join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+	print STDERR "mr $action: running >>$command<<\n" if $verbose;
+	exec($command) || die "exec: $!";
 }
 
 # work out what repos to act on
@@ -245,9 +306,12 @@ my @repos;
 my $nochdir=0;
 foreach my $topdir (sort keys %config) {
 	foreach my $subdir (sort keys %{$config{$topdir}}) {
-		next if $subdir eq 'default';
-		my $dir=$topdir.$subdir;
-		next if $dir ne $directory && $dir !~ /^\Q$directory\E\//;
+		next if $subdir eq 'DEFAULT';
+		my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
+		my $d=$directory;
+		$dir.="/" unless $dir=~/\/$/;
+		$d.="/" unless $d=~/\/$/;
+		next if $dir ne $directory && $dir !~ /^\Q$directory\E/;
 		push @repos, [$dir, $topdir, $subdir];
 	}
 }
@@ -255,8 +319,8 @@ if (! @repos) {
 	# fallback to find a leaf repo
 	LEAF: foreach my $topdir (reverse sort keys %config) {
 		foreach my $subdir (reverse sort keys %{$config{$topdir}}) {
-			next if $subdir eq 'default';
-			my $dir=$topdir.$subdir;
+			next if $subdir eq 'DEFAULT';
+			my $dir=($subdir =~/^\//) ? $subdir : $topdir.$subdir;
 			my $d=$directory;
 			$dir.="/" unless $dir=~/\/$/;
 			$d.="/" unless $d=~/\/$/;
@@ -278,7 +342,23 @@ sub action {
 	my ($action, $dir, $topdir, $subdir) = @_;
 	
 	my $lib= exists $config{$topdir}{$subdir}{lib} ?
-	                $config{$topdir}{$subdir}{lib} : "";
+	                $config{$topdir}{$subdir}{lib}."\n" : "";
+
+	if (exists $config{$topdir}{$subdir}{deleted}) {
+		if (! -d $dir) {
+			next;
+		}
+		else {
+			my $test="set -e;".$lib.$config{$topdir}{$subdir}{deleted};
+			print "mr $action: running deleted test >>$test<<\n" if $verbose;
+			my $ret=system($test);
+			if ($ret >> 8 == 0) {
+				print STDERR "mr error: $dir should be deleted yet still exists\n\n";
+				push @failed, $dir;
+				return;
+			}
+		}
+	}
 
 	if ($action eq 'checkout') {
 		if (-d $dir) {
@@ -295,38 +375,41 @@ sub action {
 	}
 	
 	$ENV{MR_REPO}=$dir;
-	if (! $nochdir && ! chdir($dir)) {
-		print STDERR "mr $action: failed to chdir to $dir: $!\n";
-		push @skipped, $dir;
-	}
 
 	if (exists $config{$topdir}{$subdir}{skip}) {
-		my $ret=system($lib.$config{$topdir}{$subdir}{skip});
+		my $test="set -e;".$lib.$config{$topdir}{$subdir}{skip};
+		print "mr $action: running skip test >>$test<<\n" if $verbose;
+		my $ret=system($test);
 		if ($ret >> 8 == 0) {
 			print "mr $action: $dir skipped per config file\n" if $verbose;
 			push @skipped, $dir;
 			return;
 		}
 	}
-
-	if (! exists $config{$topdir}{$subdir}{$action}) {
+	
+	if (! $nochdir && ! chdir($dir)) {
+		print STDERR "mr $action: failed to chdir to $dir: $!\n";
+		push @failed, $dir;
+	}
+	elsif (! exists $config{$topdir}{$subdir}{$action}) {
 		print STDERR "mr $action: no defined $action command for $topdir$subdir, skipping\n";
 		push @skipped, $dir;
 	}
 	else {
 		if (! $nochdir) {
-			print "mr $action: $dir\n";
+			print "mr $action: $topdir$subdir\n";
 		}
 		else {
-			print "mr $action: $dir (in subdir $directory)\n";
+			print "mr $action: $topdir$subdir (in subdir $directory)\n";
 		}
 		my $command="set -e; ".$lib.
-			"my_action(){ $config{$topdir}{$subdir}{$action} ; }; my_action ".
+			"my_action(){ $config{$topdir}{$subdir}{$action}\n }; my_action ".
 			join(" ", map { s/\//\/\//g; s/"/\"/g; '"'.$_.'"' } @ARGV);
+		print STDERR "mr $action: running >>$command<<\n" if $verbose;
 		my $ret=system($command);
 		if ($ret != 0) {
-			print STDERR "mr $action: failed to run: $command\n" if $verbose;
-			push @failed, $topdir.$subdir;
+			print STDERR "mr $action: failed ($ret)\n" if $verbose;
+			push @failed, $dir;
 			if ($ret >> 8 != 0) {
 				print STDERR "mr $action: command failed\n";
 			}
@@ -380,7 +463,6 @@ sub loadconfig {
 		$dir="";
 	}
 	else {
-		# $f might be a symlink
 		my $absf=abs_path($f);
 		if ($loaded{$absf}) {
 			return;
@@ -399,8 +481,8 @@ sub loadconfig {
 		my $parent=$dir;
 		while ($parent=~s/^(.*)\/[^\/]+\/?$/$1/) {
 			if (exists $config{$parent} &&
-			    exists $config{$parent}{default}) {
-				$config{$dir}{default}={ %{$config{$parent}{default}} };
+			    exists $config{$parent}{DEFAULT}) {
+				$config{$dir}{DEFAULT}={ %{$config{$parent}{DEFAULT}} };
 				last;
 			}
 		}
@@ -418,31 +500,31 @@ sub loadconfig {
 			my $value=$2;
 
 			# continuation line
-			while ($value=~/(.*)\\$/) {
-				$value=$1.<$in>;
+			while ($value=~/(.*)\\$/s) {
+				$value=$1."\n".<$in>;
 				chomp $value;
 			}
 
 			if (! defined $section) {
 				die "$f line $.: parameter ($parameter) not in section\n";
 			}
-			if ($section ne 'alias' &&
+			if ($section ne 'ALIAS' &&
 			    ! exists $config{$dir}{$section} &&
-			    exists $config{$dir}{default}) {
+			    exists $config{$dir}{DEFAULT}) {
 				# copy in defaults
-				$config{$dir}{$section}={ %{$config{$dir}{default}} };
+				$config{$dir}{$section}={ %{$config{$dir}{DEFAULT}} };
 			}
-			if ($section eq 'alias') {
+			if ($section eq 'ALIAS') {
 				$alias{$parameter}=$value;
 			}
 			elsif ($parameter eq 'lib') {
-				$config{$dir}{$section}{lib}.=$value." ; ";
+				$config{$dir}{$section}{lib}.=$value."\n";
 			}
 			else {
 				$config{$dir}{$section}{$parameter}=$value;
 				$knownactions{$parameter}=1;
 				if ($parameter eq 'chain' &&
-				    length $dir && $section ne "default" &&
+				    length $dir && $section ne "DEFAULT" &&
 				    -e $dir.$section."/.mrconfig" &&
 			    	    system($value) >> 8 == 0) {
 					push @toload, $dir.$section."/.mrconfig";
@@ -460,63 +542,192 @@ sub loadconfig {
 	}
 }
 
+sub modifyconfig {
+	my $f=shift;
+	# the section to modify or add
+	my $targetsection=shift;
+	# fields to change in the section
+	# To remove a field, set its value to "".
+	my %changefields=@_;
+
+	my @lines;
+	if (-e $f) {
+		open(my $in, "<", $f) || die "mr: open $f: $!\n";
+		@lines=<$in>;
+		close $in;
+	}
+
+	my $section;
+	my @out;
+	while (@lines) {
+		$_=shift(@lines);
+
+		if (/^\s*\#/ || /^\s*$/) {
+			push @out, $_;
+		}
+		elsif (/^\s*\[([^\]]*)\]\s*$/) {
+			if (defined $section && 
+			    $section eq $targetsection) {
+				my @blanks;
+				while ($out[$#out] =~ /^\s*$/) {
+					unshift @blanks, pop @out;
+				}
+				foreach my $field (sort keys %changefields) {
+					if (length $changefields{$field}) {
+						push @out, "$field = $changefields{$field}\n";
+					}
+				}
+				push @out, @blanks;
+			}
+
+			$section=$1;
+
+			push @out, $_;
+		}
+		elsif (/^\s*(\w+)\s*=\s(.*)/) {
+			my $parameter=$1;
+			my $value=$2;
+
+			# continuation line
+			while ($value=~/(.*\\)$/s) {
+				$value=$1."\n".shift(@lines);
+				chomp $value;
+			}
+
+			if ($section eq $targetsection) {
+				if (exists $changefields{$parameter}) {
+					if (length $changefields{$parameter}) {
+						$value=$changefields{$parameter};
+					}
+					delete $changefields{$parameter};
+				}
+			}
+
+			push @out, "$parameter = $value\n";
+		}
+	}
+
+	if (%changefields) {
+		push @out, "\n[$targetsection]\n";
+		foreach my $field (sort keys %changefields) {
+			if (length $changefields{$field}) {
+				push @out, "$field = $changefields{$field}\n";
+			}
+		}
+	}
+
+	open(my $out, ">", $f) || die "mr: write $f: $!\n";
+	print $out @out;
+	close $out;	
+}
+
 # Finally, some useful actions that mr knows about by default.
 # These can be overridden in ~/.mrconfig.
 __DATA__
-[alias]
+[ALIAS]
 	co = checkout
 	ci = commit
 	ls = list
-[default]
-lib = \
-	error() { \
-		echo "mr: $@" >&2; \
-		exit 1; \
+
+[DEFAULT]
+lib =							\
+	error() {					\
+		echo "mr: $@" >&2			\
+		exit 1					\
 	}
-update = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn update "$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git pull origin master "$@"; \
-	else \
-		error "unknown repo type"; \
+
+update =						\
+	if [ -d "$MR_REPO"/.svn ]; then			\
+		svn update "$@"				\
+	elif [ -d "$MR_REPO"/.git ]; then		\
+		git pull origin master "$@"		\
+	elif [ -d "$MR_REPO"/.bzr ]; then		\
+		bzr merge "$@"				\
+	elif [ -d "$MR_REPO"/CVS ]; then		\
+		cvs update "$@"				\
+	else						\
+		error "unknown repo type"		\
 	fi
-status = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn status "$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git status "$@" || true; \
-	else \
-		error "unknown repo type"; \
+status =						\
+	if [ -d "$MR_REPO"/.svn ]; then			\
+		svn status "$@"				\
+	elif [ -d "$MR_REPO"/.git ]; then		\
+		git status "$@" || true			\
+	elif [ -d "$MR_REPO"/.bzr ]; then		\
+		bzr status "$@"				\
+	elif [ -d "$MR_REPO"/CVS ]; then		\
+		cvs status "$@"				\
+	else						\
+		error "unknown repo type"		\
 	fi
-commit = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn commit "$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git commit -a "$@" && git push --all; \
-	else \
-		error "unknown repo type"; \
+commit =						\
+	if [ -d "$MR_REPO"/.svn ]; then			\
+		svn commit "$@"				\
+	elif [ -d "$MR_REPO"/.git ]; then		\
+		git commit -a "$@" && git push --all	\
+	elif [ -d "$MR_REPO"/.bzr ]; then		\
+		bzr commit "$@" && bzr push		\
+	elif [ -d "$MR_REPO"/CVS ]; then		\
+		cvs commit "$@"				\
+	else						\
+		error "unknown repo type"		\
 	fi
-diff = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn diff "$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git diff "$@"; \
-	else \
-		error "unknown repo type"; \
+diff =							\
+	if [ -d "$MR_REPO"/.svn ]; then			\
+		svn diff "$@"				\
+	elif [ -d "$MR_REPO"/.git ]; then		\
+		git diff "$@"				\
+	elif [ -d "$MR_REPO"/.bzr ]; then		\
+		bzr diff "$@"				\
+	elif [ -d "$MR_REPO"/CVS ]; then		\
+		cvs diff "$@"				\
+	else						\
+		error "unknown repo type"		\
 	fi
-log = \
-	if [ -d "$MR_REPO"/.svn ]; then \
-		svn log"$@"; \
-	elif [ -d "$MR_REPO"/.git ]; then \
-		git log "$@"; \
-	else \
-		error "unknown repo type"; \
+log =							\
+	if [ -d "$MR_REPO"/.svn ]; then			\
+		svn log"$@"				\
+	elif [ -d "$MR_REPO"/.git ]; then		\
+		git log "$@"				\
+	elif [ -d "$MR_REPO"/.bzr ]; then		\
+		bzr log "$@"				\
+	elif [ -d "$MR_REPO"/CVS ]; then		\
+		cvs log "$@"				\
+	else						\
+		error "unknown repo type"		\
+	fi
+register =								\
+	if [ -z "$1" ]; then						\
+		error "repository directory not specified"		\
+	fi								\
+	cd "$1"								\
+	basedir="$(basename $(pwd))"					\
+	if [ -d .svn ]; then						\
+		url=$(svn info . |					\
+		      grep -i ^URL: | cut -d ' ' -f 2)			\
+		if [ -z "$url" ]; then					\
+			error "cannot determine svn url"		\
+		fi							\
+		echo "Registering svn url: $url"			\
+		mr config "$(pwd)" checkout="svn co $url $basedir"	\
+	elif [ -d .git ]; then						\
+		url=$(git-config --get remote.origin.url)		\
+		if [ -z "$url" ]; then					\
+			error "cannot determine git url"		\
+		fi							\
+		echo "Registering git url: $url"			\
+		mr config "$(pwd)" checkout="git clone $url $basedir"	\
+	else								\
+		error "unable to register this repo type"		\
 	fi
 list = true
-help = \
-	if [ ! -e "$MR_PATH" ]; then \
-		error "cannot find program path";\
-	fi; \
-	(pod2man -c mr "$MR_PATH" | man -l -) || \
+config = 
+help =							\
+	if [ ! -e "$MR_PATH" ]; then			\
+		error "cannot find program path"	\
+	fi						\
+	(pod2man -c mr "$MR_PATH" | man -l -) ||	\
 		error "pod2man or man failed"
+
+ed = echo "A horse is a horse, of course, of course.."
+T = echo "I pity the fool."