X-Git-Url: https://git.madduck.net/code/myrepos.git/blobdiff_plain/2ad4c44bc925cee8ee1f0caccb3ee3b8d15acbfb..788d61bfe0b595669b6c8fd832ec6b7044520563:/mr?ds=sidebyside

diff --git a/mr b/mr
index 7147061..d8a27b2 100755
--- a/mr
+++ b/mr
@@ -1,8 +1,8 @@
-#!/usr/bin/perl
+#!/usr/bin/env perl
 
 =head1 NAME
 
-mr - a Multiple Repository management tool
+mr - a tool to manage all your version control repos
 
 =head1 SYNOPSIS
 
@@ -16,15 +16,19 @@ B<mr> [options] commit [-m "message"]
 
 B<mr> [options] record [-m "message"]
 
+B<mr> [options] fetch
+
 B<mr> [options] push
 
 B<mr> [options] diff
 
 B<mr> [options] log
 
+B<mr> [options] grep pattern
+
 B<mr> [options] run command [param ...]
 
-B<mr> [options] bootstrap url [directory]
+B<mr> [options] bootstrap src [directory]
 
 B<mr> [options] register [repository]
 
@@ -38,11 +42,11 @@ B<mr> [options] remember action [params ...]
 
 =head1 DESCRIPTION
 
-B<mr> is a Multiple Repository management tool. It can checkout, update, or
-perform other actions on a set of repositories as if they were one combined
-repository. It supports any combination of subversion, git, cvs, mercurial,
-bzr, darcs, fossil and veracity repositories, and support for other version
-control systems can easily be added.
+B<mr> is a tool to manage all your version control repos. It can checkout,
+update, or perform other actions on a set of repositories as if they were
+one combined repository. It supports any combination of subversion, git,
+cvs, mercurial, bzr, darcs, fossil and veracity repositories, and support
+for other version control systems can easily be added.
 
 B<mr> cds into and operates on all registered repositories at or below your
 working directory. Or, if you are in a subdirectory of a repository that
@@ -73,7 +77,8 @@ If a repository isn't checked out yet, it will first check it out.
 =item status
 
 Displays a status report for each repository, showing what
-uncommitted changes are present in the repository.
+uncommitted changes are present in the repository. For distributed version
+control systems, also shows unpushed local branches.
 
 =item commit (or ci)
 
@@ -91,6 +96,12 @@ remote repository. Only supported for distributed version control systems.
 
 The optional -m parameter allows specifying a commit message.
 
+=item fetch
+
+Fetches from each repository's remote repository, but does not
+update the working copy. Only supported for some distributed version
+control systems.
+
 =item push
 
 Pushes committed local changes to the remote repository. A no-op for
@@ -104,6 +115,11 @@ Show a diff of uncommitted changes.
 
 Show the commit log.
 
+=item grep pattern
+
+Searches for a pattern in each repository using the grep subcommand. Uses
+ack-grep on VCS that do not have their own.
+
 =item run command [param ...]
 
 Runs the specified command in each repository.
@@ -114,15 +130,39 @@ These commands are also available:
 
 =over 4
 
-=item bootstrap url [directory]
+=item bootstrap src [directory]
+
+Causes mr to retrieve the source C<src> and use it as a .mrconfig file to
+checkout the repositories listed in it, into the specified directory.
+
+B<mr> understands several types of sources:
+
+=over 4
+
+=item URL for curl
+
+C<src> may be an URL understood by B<curl>.
+
+=item copy via ssh
 
-Causes mr to download the url, and use it as a .mrconfig file
-to checkout the repositories listed in it, into the specified directory.
+To use B<scp> to download, the C<src> may have the form
+C<ssh://[user@]host:file>.
+
+=item local file
+
+You can retrieve the config file by other means and pass its B<path> as C<src>.
+
+=item standard input
+
+If source C<src> consists in a single dash C<->, config file is read from
+standard input.
+
+=back
 
 The directory will be created if it does not exist. If no directory is
 specified, the current directory will be used.
 
-If the .mrconfig file includes a repository named ".", that
+As a special case, if source C<src> includes a repository named ".", that
 is checked out into the top of the specified directory.
 
 =item list (or ls)
@@ -225,6 +265,13 @@ configuration.
 
 Be verbose.
 
+=item -m
+
+=item --minimal
+
+Minimise output. If a command fails or there is any output then the usual
+output will be shown.
+
 =item -q
 
 =item --quiet
@@ -508,7 +555,7 @@ Copyright 2007-2011 Joey Hess <joey@kitenet.net>
 
 Licensed under the GNU GPL version 2 or higher.
 
-http://kitenet.net/~joey/code/mr/
+http://myrepos.branchable.com/
 
 =cut
 
@@ -528,6 +575,7 @@ use constant {
 # configurables
 my $config_overridden=0;
 my $verbose=0;
+my $minimal=0;
 my $quiet=0;
 my $stats=0;
 my $force=0;
@@ -538,6 +586,7 @@ my $no_chdir=0;
 my $jobs=1;
 my $trust_all=0;
 my $directory=getcwd();
+my $terminal=-t STDOUT && eval{require IO::Pty::Easy;IO::Pty::Easy->import();1;};
 
 my $HOME_MR_CONFIG = "$ENV{HOME}/.mrconfig";
 $ENV{MR_CONFIG}=find_mrconfig();
@@ -681,9 +730,38 @@ sub fulldir {
 	return $subdir =~ /^\// ? $subdir : $topdir.$subdir;
 }
 
+sub terminal_friendly_spawn {
+	my $actionmsg = shift;
+	my $sh = shift;
+	my $quiet = shift;
+	my $minimal = shift;
+	my $output = "";
+	if ($terminal) {
+		my $pty = IO::Pty::Easy->new;
+		$pty->spawn($sh);
+		while ($pty->is_active) {
+			my $data = $pty->read();
+			$output .= $data if defined $data;
+		}
+		$pty->close;
+	} else {
+		$output = qx/$sh 2>&1/;
+	}
+	my $ret = $?;
+	if ($quiet && $ret != 0) {
+		print "$actionmsg\n" if $actionmsg;
+		print STDERR $output;
+	} elsif (!$quiet && (!$minimal || $output)) {
+		print "$actionmsg\n" if $actionmsg;
+		print $output;
+	}
+	return ($ret, $output ? 1 : 0);
+}
+
 sub action {
 	my ($action, $dir, $topdir, $subdir, $force_checkout) = @_;
 	my $fulldir=fulldir($topdir, $subdir);
+	my $checkout_dir;
 
 	$ENV{MR_CONFIG}=$configfiles{$topdir};
 	my $is_checkout=($action eq 'checkout');
@@ -725,6 +803,7 @@ sub action {
 	}
 
 	if ($is_checkout) {
+		$checkout_dir=$dir;
 		if (! $force_checkout) {
 			if (-d $dir) {
 				print "mr $action: $dir already exists, skipping checkout\n" if $verbose;
@@ -758,7 +837,7 @@ sub action {
 			return FAILED;
 		}
 		else {
-			print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n";
+			print STDERR "mr $action: no defined action for $vcs repository $fulldir, skipping\n" unless $minimal;
 			return SKIPPED;
 		}
 	}
@@ -772,22 +851,16 @@ sub action {
 			$s=~s/^\Q$fulldir\E\/?//;
 			$actionmsg="mr $action: $fulldir (in subdir $s)";
 		}
-		print "$actionmsg\n" unless $quiet;
+		print "$actionmsg\n" unless $quiet || $minimal;
 
-		my $hookret=hook("pre_$action", $topdir, $subdir);
+		my ($hookret, $hook_out)=hook("pre_$action", $topdir, $subdir);
 		return $hookret if $hookret != OK;
 
-		my $ret=runsh $action, $topdir, $subdir,
+		my ($ret, $out)=runsh $action, $topdir, $subdir,
 			$command, \@ARGV, sub {
 				my $sh=shift;
-				if ($quiet) {
-					my $output = qx/$sh 2>&1/;
-					my $ret = $?;
-					if ($ret != 0) {
-						print "$actionmsg\n";
-						print STDERR $output;
-					}
-					return $ret;
+				if (!$jobs || $jobs > 1 || $quiet || $minimal) {
+					return terminal_friendly_spawn($actionmsg, $sh, $quiet, $minimal);
 				}
 				else {
 					system($sh);
@@ -825,15 +898,21 @@ sub action {
 				return FAILED;
 			}
 
-			my $ret=hook("post_$action", $topdir, $subdir);
+			my ($ret, $hook_out)=hook("post_$action", $topdir, $subdir);
 			return $ret if $ret != OK;
 			
-			if (($is_checkout || $is_update)) {
-				my $ret=hook("fixups", $topdir, $subdir);
+			if ($is_checkout || $is_update) {
+				if ($is_checkout && ! $no_chdir) {
+					if (! chdir($checkout_dir)) {
+				                print STDERR "mr $action: failed to chdir to $checkout_dir: $!\n";
+						return FAILED;
+					}
+				}
+				my ($ret, $hook_out)=hook("fixups", $topdir, $subdir);
 				return $ret if $ret != OK;
 			}
 			
-			return OK;
+			return (OK, $out || $hook_out);
 		}
 	}
 }
@@ -843,15 +922,10 @@ sub hook {
 
 	my $command=$config{$topdir}{$subdir}{$hook};
 	return OK unless defined $command;
-	my $ret=runsh $hook, $topdir, $subdir, $command, [], sub {
+	my ($ret,$out)=runsh $hook, $topdir, $subdir, $command, [], sub {
 			my $sh=shift;
-			if ($quiet) {
-				my $output = qx/$sh 2>&1/;
-				my $ret = $?;
-				if ($ret != 0) {
-					print STDERR $output;
-				}
-				return $ret;
+			if (!$jobs || $jobs > 1 || $quiet || $minimal) {
+				return terminal_friendly_spawn(undef, $sh, $quiet, $minimal);
 			}
 			else {
 				system($sh);
@@ -871,7 +945,7 @@ sub hook {
 		}
 	}
 
-	return OK;
+	return (OK, $out);
 }
 
 # run actions on multiple repos, in parallel
@@ -899,7 +973,7 @@ sub mrs {
 				close CHILD_STDERR;
 				close $outfh;
 				close $errfh;
-				exit action($action, @$repo);
+				exit +(action($action, @$repo))[0];
 			}
 			close CHILD_STDOUT;
 			close CHILD_STDERR;
@@ -930,7 +1004,7 @@ sub mrs {
 						    	waitpid($active[$i][0], 0);
 							print STDOUT $out[$i][0];
 							print STDERR $out[$i][1];
-							record($active[$i][1], $? >> 8);
+							record($active[$i][1], $? >> 8, $out[$i][0] || $out[$i][1]);
 						    	splice(@fhs, $i, 1);
 						    	splice(@active, $i, 1);
 						    	splice(@out, $i, 1);
@@ -947,10 +1021,11 @@ sub mrs {
 sub record {
 	my $dir=shift()->[0];
 	my $ret=shift;
+	my $out=shift;
 
 	if ($ret == OK) {
 		push @ok, $dir;
-		print "\n" unless $quiet;
+		print "\n" unless $quiet || ($minimal && !$out);
 	}
 	elsif ($ret == FAILED) {
 		if ($interactive) {
@@ -959,7 +1034,7 @@ sub record {
 			system((getpwuid($<))[8], "-i");
 		}
 		push @failed, $dir;
-		print "\n" unless $quiet;
+		print "\n";
 	}
 	elsif ($ret == SKIPPED) {
 		push @skipped, $dir;
@@ -981,10 +1056,10 @@ sub showstats {
 		showstat($#ok+1, "ok", "ok"),
 		showstat($#failed+1, "failed", "failed"),
 		showstat($#skipped+1, "skipped", "skipped"),
-	).")\n" unless $quiet;
+	).")\n" unless $quiet || $minimal;
 	if ($stats) {
 		if (@skipped) {
-			print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet;
+			print "mr $action: (skipped: ".join(" ", @skipped).")\n" unless $quiet || $minimal;
 		}
 		if (@failed) {
 			print STDERR "mr $action: (failed: ".join(" ", @failed).")\n";
@@ -1106,7 +1181,8 @@ sub is_trusted_config {
 			while (<TRUST>) {
 				chomp;
 				s/^~\//$ENV{HOME}\//;
-				$trusted{abs_path($_)}=1;
+				my $d=abs_path($_);
+				$trusted{$d}=1 if defined $d;
 			}
 			close TRUST;
 		}
@@ -1197,7 +1273,7 @@ my %loaded;
 sub loadconfig {
 	my $f=shift;
 	my $dir=shift;
-	my $bootstrap_url=shift;
+	my $bootstrap_src=shift;
 
 	my @toload;
 
@@ -1285,8 +1361,8 @@ sub loadconfig {
 	my $trusterror = sub {
 		my $msg=shift;
 	
-		if (defined $bootstrap_url) {
-			die "mr: $msg in untrusted $bootstrap_url line $lineno\n".
+		if (defined $bootstrap_src) {
+			die "mr: $msg in untrusted $bootstrap_src line $lineno\n".
 				"(To trust this url, --trust-all can be used; but please use caution;\n".
 				"this can allow arbitrary code execution!)\n";
 		}
@@ -1299,11 +1375,12 @@ sub loadconfig {
 	while (@lines) {
 		$_=$nextline->();
 
+		next if /^\s*\#/ || /^\s*$/;
+
 		if (! $trusted && /[[:cntrl:]]/) {
 			$trusterror->("illegal control character");
 		}
 
-		next if /^\s*\#/ || /^\s*$/;
 		if (/^\[([^\]]*)\]\s*$/) {
 			$section=$1;
 
@@ -1524,10 +1601,7 @@ sub dispatch {
 	my $action=shift;
 
 	# actions that do not operate on all repos
-	if ($action eq 'help') {
-		help(@ARGV);
-	}
-	elsif ($action eq 'config') {
+	if ($action eq 'config') {
 		config(@ARGV);
 	}
 	elsif ($action eq 'register') {
@@ -1555,7 +1629,27 @@ sub dispatch {
 }
 
 sub help {
-	exec($config{''}{DEFAULT}{help}) || die "exec: $!";
+	my $help=q#
+		case `uname -s` in
+			SunOS)
+			SHOWMANFILE="man -f"
+			;;
+			Darwin)
+			SHOWMANFILE="man"
+			;;
+			*)
+			SHOWMANFILE="man"
+			;;
+		esac
+		if [ ! -e "$MR_PATH" ]; then
+			error "cannot find program path"
+		fi
+		tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
+		trap "rm -f $tmp" exit
+		pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
+		$SHOWMANFILE "$tmp" || error "man failed"
+	#;
+	exec($help) || die "exec: $!";
 }
 
 sub config {
@@ -1642,23 +1736,47 @@ sub register {
 }
 
 sub bootstrap {
-	my $url=shift @ARGV;
+	eval q{use File::Copy};
+	die $@ if $@;
+
+	my $src=shift @ARGV;
 	my $dir=shift @ARGV || ".";
 	
-	if (! defined $url || ! length $url) {
-		die "mr: bootstrap requires url\n";
+	if (! defined $src || ! length $src) {
+		die "mr: bootstrap requires source\n";
 	}
-	
-	# Download the config file to a temporary location.
+
+	# Retrieve config file.
 	eval q{use File::Temp};
 	die $@ if $@;
 	my $tmpconfig=File::Temp->new();
-	my @curlargs = ("curl", "-A", "mr", "-L", "-s", $url, "-o", $tmpconfig);
-	push(@curlargs, "-k") if $insecure;
-	my $curlstatus = system(@curlargs);
-	die "mr bootstrap: invalid SSL certificate for $url (consider -k)\n" if $curlstatus >> 8 == 60;
-	die "mr bootstrap: download of $url failed\n" if $curlstatus != 0;
+	if ($src =~ m!^[\w\d]+://!) {
+		# Download the config file to a temporary location.
+		my @downloader;
+		if ($src =~ m!^ssh://(.*)!) {
+			@downloader = ("scp", $1, $tmpconfig);
+		}
+		else {
+			@downloader = ("curl", "-A", "mr", "-L", "-s", $src, "-o", $tmpconfig);
+			push(@downloader, "-k") if $insecure;
+		}
+		my $status = system(@downloader);
+		die "mr bootstrap: invalid SSL certificate for $src (consider -k)\n"
+			if $downloader[0] eq 'curl' && $status >> 8 == 60;
+		die "mr bootstrap: download of $src failed\n" if $status != 0;
+	}
+	elsif ($src eq '-') {
+		# Config file is read from stdin.
+		copy(\*STDIN, $tmpconfig) || die "stdin: $!";
+	}
+	else {
+		# Config file is local.
+		die "mr bootstrap: cannot read file '$src'"
+			unless -r $src;
+		copy($src, $tmpconfig) || die "copy: $!";
+	}
 
+	# Sanity check on destination directory.
 	if (! -e $dir) {
 		system("mkdir", "-p", $dir);
 	}
@@ -1668,16 +1786,14 @@ sub bootstrap {
 	# would normally be skipped.
 	my $topdir=abs_path(".")."/";
 	my @repo=($topdir, $topdir, ".");
-	loadconfig($tmpconfig, $topdir, $url);
+	loadconfig($tmpconfig, $topdir, $src);
 	record(\@repo, action("checkout", @repo, 1))
 		if exists $config{$topdir}{"."}{"checkout"};
 
 	if (-e ".mrconfig") {
-		print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $url\n";
+		print STDERR "mr bootstrap: .mrconfig file already exists, not overwriting with $src\n";
 	}
 	else {
-		eval q{use File::Copy};
-		die $@ if $@;
 		move($tmpconfig, ".mrconfig") || die "rename: $!";
 	}
 
@@ -1735,6 +1851,7 @@ sub getopts {
 		"p|path" => sub { }, # now default, ignore
 		"f|force" => \$force,
 		"v|verbose" => \$verbose,
+		"m|minimal" => \$minimal,
 		"q|quiet" => \$quiet,
 		"s|stats" => \$stats,
 		"k|insecure" => \$insecure,
@@ -1791,6 +1908,7 @@ sub exitstats {
 sub main {
 	getopts();
 	init();
+	help(@ARGV) if $ARGV[0] eq 'help';
 
 	startingconfig();
 	loadconfig($HOME_MR_CONFIG);
@@ -1858,7 +1976,7 @@ lib =
 	}
 
 svn_test = perl: -d "$ENV{MR_REPO}/.svn"
-git_test = perl: -d "$ENV{MR_REPO}/.git"
+git_test = perl: -e "$ENV{MR_REPO}/.git"
 bzr_test = perl: -d "$ENV{MR_REPO}/.bzr"
 cvs_test = perl: -d "$ENV{MR_REPO}/CVS"
 hg_test  = perl: -d "$ENV{MR_REPO}/.hg"
@@ -1882,21 +2000,26 @@ bzr_update =
 	else
 		bzr merge --pull "$@"
 	fi
-cvs_update = cvs update "$@"
-hg_update  = hg pull "$@" && hg update "$@"
+cvs_update = cvs -q update "$@"
+hg_update  = hg pull "$@"; hg update "$@"
 darcs_update = darcs pull -a "$@"
 fossil_update = fossil pull "$@"
 vcsh_update = vcsh run "$MR_REPO" git pull "$@"
 veracity_update = vv pull "$@" && vv update "$@"
 
+git_fetch = git fetch --all --prune --tags
+git_svn_fetch = git svn fetch
+darcs_fetch = darcs fetch
+hg_fetch = hg pull
+
 svn_status = svn status "$@"
-git_status = git status -s "$@" || true
-bzr_status = bzr status --short "$@"
-cvs_status = cvs status "$@"
-hg_status  = hg status "$@"
+git_status = git status -s "$@" || true; git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline || true
+bzr_status = bzr status --short "$@"; bzr missing
+cvs_status = cvs -q status | grep -E '^(File:.*Status:|\?)' | grep -v 'Status: Up-to-date'
+hg_status  = hg status "$@"; hg summary --quiet | grep -v 'parent: 0:'
 darcs_status = darcs whatsnew -ls "$@" || true
 fossil_status = fossil changes "$@"
-vcsh_status = cd $(vcsh run "$MR_REPO" git config --get core.worktree); vcsh run "$MR_REPO" git status -s "$@" || true
+vcsh_status = vcsh run "$MR_REPO" git -c status.relativePaths=false status -s "$@" || true
 veracity_status = vv status "$@"
 
 svn_commit = svn commit "$@"
@@ -1908,11 +2031,11 @@ bzr_commit =
 		bzr commit "$@" && bzr push
 	fi
 cvs_commit = cvs commit "$@"
-hg_commit  = hg commit -m "$@" && hg push
-darcs_commit = darcs record -a -m "$@" && darcs push -a
+hg_commit  = hg commit "$@" && hg push
+darcs_commit = darcs record -a "$@" && darcs push -a
 fossil_commit = fossil commit "$@"
 vcsh_commit = vcsh run "$MR_REPO" git commit -a "$@" && vcsh run "$MR_REPO" git push --all
-veracity_commit = vv commit -m "@" && vv push
+veracity_commit = vv commit "$@" && vv push
 
 git_record = git commit -a "$@"
 bzr_record =
@@ -1921,11 +2044,11 @@ bzr_record =
 	else
 		bzr commit "$@"
 	fi
-hg_record  = hg commit -m "$@"
-darcs_record = darcs record -a -m "$@"
+hg_record  = hg commit "$@"
+darcs_record = darcs record -a "$@"
 fossil_record = fossil commit "$@"
 vcsh_record = vcsh run "$MR_REPO" git commit -a "$@"
-veracity_record = vv commit -m "@"
+veracity_record = vv commit "$@"
 
 svn_push = :
 git_push = git push "$@"
@@ -1940,7 +2063,7 @@ veracity_push = vv push "$@"
 svn_diff = svn diff "$@"
 git_diff = git diff "$@"
 bzr_diff = bzr diff "$@"
-cvs_diff = cvs diff "$@"
+cvs_diff = cvs -q diff "$@"
 hg_diff  = hg diff "$@"
 darcs_diff = darcs diff -u "$@"
 fossil_diff = fossil diff "$@"
@@ -1958,6 +2081,13 @@ fossil_log = fossil timeline "$@"
 vcsh_log = vcsh run "$MR_REPO" git log "$@"
 veracity_log = vv log "$@"
 
+hg_grep = hg grep "$@"
+cvs_grep = ack-grep "$@"
+svn_grep = ack-grep "$@"
+git_svn_grep = git grep "$@"
+git_grep = git grep "$@"
+bzr_grep = ack-grep "$@"
+
 run = "$@"
 
 svn_register =
@@ -1975,7 +2105,7 @@ git_register =
 	echo "Registering git url: $url in $MR_CONFIG"
 	mr -c "$MR_CONFIG" config "`pwd`" checkout="git clone '$url' '$MR_REPO'"
 bzr_register =
-	url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}'`"
+	url="`LC_ALL=C bzr info . | egrep -i 'checkout of branch|parent branch' | awk '{print $NF}' | head -n 1`"
 	if [ -z "$url" ]; then
 		error "cannot determine bzr url"
 	fi
@@ -2030,30 +2160,12 @@ bzr_trusted_checkout = bzr checkout|clone|branch|get $url $repo
 hg_trusted_checkout = hg clone $url $repo
 darcs_trusted_checkout = darcs get $url $repo
 git_bare_trusted_checkout = git clone --bare $url $repo
-vcsh_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
+vcsh_old_trusted_checkout = vcsh run "$MR_REPO" git clone $url $repo
+vcsh_trusted_checkout = vcsh clone $url $repo
 # fossil: messy to do
 veracity_trusted_checkout = vv clone $url $repo
 
 
-help =
- 	case `uname -s` in
-		SunOS)
-		SHOWMANFILE="man -f"
-		;;
-		Darwin)
-		SHOWMANFILE="man"
-		;;
-		*)
-		SHOWMANFILE="man -l"
-		;;
-	esac
-	if [ ! -e "$MR_PATH" ]; then
-		error "cannot find program path"
-	fi
-	tmp=$(mktemp -t mr.XXXXXXXXXX) || error "mktemp failed"
-	trap "rm -f $tmp" exit
-	pod2man -c mr "$MR_PATH" > "$tmp" || error "pod2man failed"
-	$SHOWMANFILE "$tmp" || error "man failed"
 list = true
 config = 
 bootstrap =