From 5a3eb9fe730018c698ac32be9cc12021393fb90f Mon Sep 17 00:00:00 2001 From: "Kyle J. McKay" Date: Tue, 17 Aug 2021 20:06:15 -0700 Subject: [PATCH] projtool.pl: add worktree subcommand Provide a worktree subcommand for projtool.pl to make it much simpler to add a worktree to a project. Simply pass the arguments along to the `git worktree` command together with a --git-dir= option. However, as a special case, if the project is bare (the usual case) and its HEAD branch has not been checked out in any worktree and no explicit branch name was given, just automagically DWIM and supply the branch name when passing along the command. And at the same time make it work for an unborn branch too. Signed-off-by: Kyle J. McKay --- toolbox/projtool.pl | 111 +++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 110 insertions(+), 1 deletion(-) diff --git a/toolbox/projtool.pl b/toolbox/projtool.pl index 77c83d5..af61714 100755 --- a/toolbox/projtool.pl +++ b/toolbox/projtool.pl @@ -10,12 +10,16 @@ use strict; use warnings; use vars qw($VERSION); -BEGIN {*VERSION = \'1.0.6'} +BEGIN {*VERSION = \'1.0.7'} use File::Basename; use Digest::MD5 qw(md5_hex); use IO::Socket; use Cwd qw(realpath); use POSIX qw(strftime); + +my $origHOME; +BEGIN {$origHOME = $ENV{HOME}} + use lib "__BASEDIR__"; use Girocco::Config; use Girocco::Util; @@ -117,6 +121,17 @@ Usage: %s [...] an error (unless --quiet is used). Exit status will be 0 if project found, non-zero otherwise. + worktree [--force] + run 'git --git-dir= worktree ' + except that if consists of the + subcommand "add" and a single non-option argument and the + 's git-dir is bare (the usual case) and the 's + HEAD branch is not already checked out in any other worktree, + then suitable arguments will be passed to the `worktree` + command to make the newly created worktree checkout the HEAD + branch of the project (special logic makes that work even + for an unborn HEAD). + urls [--push] show available fetch/push URLs for Note that this does NOT include non-Git protocol URLs such @@ -1244,6 +1259,98 @@ sub cmd_verify { return 0; } +sub get_worktrees { + my $gd = shift; + return () unless -d "$gd/worktrees"; + opendir my $dh, "$gd/worktrees" or return (); + my @dirs = grep { !/^\.\.?$/ && -d "$gd/worktrees/$_"} readdir($dh); + closedir $dh; + my @wt = (); + foreach (@dirs) { + open my $hd, '<', "$gd/worktrees/$_/HEAD" or next; + my $hh = <$hd>; + close $hd; + defined($hh) or next; + chomp($hh); + if ($hh =~ /^[0-9a-f]{40,}$/) { + push(@wt, [$_, $hh]); + } elsif ($hh =~ m{^ref:\s?(refs/heads/.+)$}) { + push(@wt, [$_, $1]); + } + } + return @wt; +} + +sub cmd_worktree { + eval '$Girocco::Config::var_have_git_250' or die "worktree requires Git 2.5.0 or later\n"; + my $force; + parse_options("force" => \$force, quiet => \$quiet, q =>\$quiet); + @ARGV >= 2 or die_usage; + my $project = get_project_harder($ARGV[0]); + my $gd = $project->{path}; + defined($gd) && -d $gd or die "Project \"$$project{name}\" does not actually exist\n"; + if ($project->{mirror}) { + die "Cannot use worktree command on mirror project\n" unless $force; + warn "Continuing with --force even though project is a mirror\n" unless $quiet; + } + shift @ARGV; + my $symref = undef; + my $mthash = undef; + my $wantdwimadd = 0; + my $hb = $project->{HEAD}; + defined($hb) or $hb = ""; + $hb !~ /^\[/ or $hb = ""; + if ($ARGV[0] eq "add" && (@ARGV == 2 && $ARGV[1] !~ /^-/ || + @ARGV == 3 && $ARGV[1] !~ /^-/ && $hb && $ARGV[2] eq $hb)) {{ + $wantdwimadd = -1; + # Only "add" subcommand has special handling + my $isbare = get_git_chomp("--git-dir=".$gd, "rev-parse", "--is-bare-repository"); + defined($isbare) && $isbare eq "true" or last; # only for bare repos + $symref = get_git_chomp("--git-dir=".$gd, "symbolic-ref", "-q", "HEAD"); + defined($symref) && $symref =~ m{^refs/heads/.} or last; # only if symref HEAD + !grep({$$_[1] eq $symref} get_worktrees($gd)) or last; # and not checked out + if (get_git_chomp("--git-dir=".$gd, "rev-parse", "--verify", "-q", $symref)) { + # easy case, branch already exists, just add its name to arg list + push(@ARGV, substr($symref, 11)) unless @ARGV == 3; + $symref = undef; + $wantdwimadd = 1; + } else { + # nasty workaround for broken worktree command + my $mttree = get_git_chomp("--git-dir=".$gd, "mktree"); + defined($mttree) && $mttree =~ /^[0-9a-f]{40,}$/ or last; + my $now = time(); + my $cmt = "tree $mttree\nauthor - <-> $now +0000\ncommitter - <-> $now +0000\n\n"; + my ($st, $rslt) = capture_command(1, $cmt, $Girocco::Config::git_bin, + "--git-dir=".$gd, "hash-object", "-t", "commit", "-w", "--stdin"); + defined($st) && $st == 0 && defined($rslt) or last; + chomp($rslt); + $rslt =~ /^[0-9a-f]{40,}$/ or last; + $mthash = $rslt; + pop(@ARGV) if @ARGV == 3; + push(@ARGV, $mthash); # requires ugly fixup afterwards + $wantdwimadd = 2; + } + }} + warn $project->{name}, ": cannot DWIM worktree add (check `worktree list` output)\n" + if $wantdwimadd == -1 && !$quiet; + my $saveHOME = $ENV{HOME}; + if (defined($origHOME) && $origHOME ne "" && $origHOME =~ m{^(/.+)$} && -d $1) { + $ENV{HOME} = $1; + } + my $ec = system($Girocco::Config::git_bin, "--git-dir=".$gd, "worktree", @ARGV); + $ENV{HOME} = $saveHOME; + if (defined($ec) && $ec == 0 && defined($symref) && defined($mthash)) { + # ugly fixup time + foreach (map($$_[0], grep({$$_[1] eq $mthash} get_worktrees($gd)))) { + open my $hf, '>', "$gd/worktrees/$_/HEAD" or next; + print $hf "ref: $symref\n"; + close $hf; + } + } + defined($ec) && $ec != -1 or return 1; + return ($ec >> 8); +} + sub cmd_urls { my $pushonly; parse_options("push" => \$pushonly); @@ -2123,6 +2230,7 @@ BEGIN { prune => \&cmd_prune, show => \&cmd_show, verify => \&cmd_verify, + worktree => \&cmd_worktree, urls => \&cmd_urls, listheads => \&cmd_listheads, listtags => \&cmd_listtags, @@ -2194,6 +2302,7 @@ BEGIN { %nopager = ( set => -1, urls => -1, verify => 1, + worktree => 1, )} sub dohelp { -- 2.11.4.GIT