diff options
-rwxr-xr-x | bin/annex-review-unused | 36 | ||||
-rw-r--r-- | lib/perl5/Local/MrRepo/Repo/Git/Annex.pm | 152 |
2 files changed, 144 insertions, 44 deletions
diff --git a/bin/annex-review-unused b/bin/annex-review-unused new file mode 100755 index 00000000..c375a40e --- /dev/null +++ b/bin/annex-review-unused @@ -0,0 +1,36 @@ +#!/usr/bin/perl + +# annex-review-unused -- interactively process `git annex unused` output + +# Copyright (C) 2019 Sean Whitton +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 3 of the License, or (at +# your option) any later version. +# +# This program is distributed in the hope that it will be useful, but +# WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +# General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +use 5.028; +use strict; +use warnings; +use lib "$ENV{HOME}/lib/perl5"; + +use Getopt::Long; +use Local::MrRepo; + +die 'not an annex root' unless -e '.git/annex'; +my $mr_repo = Local::MrRepo::new_repo('.'); +die "couldn't initialise Local::MrRepo::Repo::Git::Annex object" + unless $mr_repo->can('review_unused'); +my %review_unused_args = (interactive => 1); +my $from_arg; +GetOptions('from:s' => \$from_arg); +$review_unused_args{from} = $from_arg if defined $from_arg; +$mr_repo->review_unused(%review_unused_args); diff --git a/lib/perl5/Local/MrRepo/Repo/Git/Annex.pm b/lib/perl5/Local/MrRepo/Repo/Git/Annex.pm index 6fbe616d..5d379ef9 100644 --- a/lib/perl5/Local/MrRepo/Repo/Git/Annex.pm +++ b/lib/perl5/Local/MrRepo/Repo/Git/Annex.pm @@ -22,10 +22,13 @@ use lib "$ENV{HOME}/lib/perl5"; use parent 'Local::MrRepo::Repo::Git'; use Exporter 'import'; +use File::Spec::Functions qw(rel2abs); use Git::Wrapper; use JSON; use Local::ScriptStatus; use Try::Tiny; +use Term::ReadKey; +use Local::Interactive qw(prompt); our @EXPORT_OK = (); @@ -59,56 +62,117 @@ sub review { my ($review_unused) = $self->git->config(qw(--local --get --type=bool --default true mrrepo.review-unused)); - if ($review_unused eq 'true') { - my @unused_lines = - $self->git->annex("unused", - { used_refspec - => "+refs/heads/*:-refs/heads/synced/*" }); - my @unused_files = - map { /^ ([0-9]+) +([^ ]+)$/ ? {number => $1, key => $2} : ()} - @unused_lines; - unless (@unused_files == 0) { - say_spaced_bullet("There are unused files you can drop with" - . " `git annex dropunused':"); - say for @unused_lines; + $issues = $self->review_unused(interactive => 0) || $issues + if $review_unused eq 'true'; + + return $issues; +} + +sub review_unused { + my $self = shift; + my %opts = @_; + $opts{interactive} //= 0; + $opts{used_refspec} //= "+refs/heads/*:-refs/heads/synced/*"; + + my %unused_args = (used_refspec => $opts{used_refspec}); + my %dropunused_args = (force => 1); + $unused_args{from} = $dropunused_args{from} = $opts{from} + if defined $opts{from}; + + my @to_drop = (); + my @unused_lines = $self->git->annex("unused", \%unused_args); + my @unused_files + = map { /^ ([0-9]+) +([^ ]+)$/ ? { number => $1, key => $2 } : () } + @unused_lines; + return 0 if @unused_files == 0; + unless ($opts{interactive}) { + say_spaced_bullet("There are unused files you can drop with" + . " `git annex dropunused':"); + say for @unused_lines; + say ""; + } + + foreach my $unused_file (@unused_files) { + system('clear', '-x') if $opts{interactive}; + say_bold("unused file #" . $unused_file->{number} . ":"); + + # this way of determining whether a file is tmp or bad, rather + # than really unused, is not great because it only works for + # the local repo and makes unneeded `ga contentlocation` + # calls. it would be better just to parse @unused_lines with + # a proper state machine and thereby extract into separate + # arrays the unused and the tmp/bad data + my $is_tmp_or_bad = 0; + my $content_location; + try { + ($content_location) + = $self->git->annex("contentlocation", $unused_file->{key}); + $content_location = rel2abs($content_location, $self->toplevel); + } + catch { + $is_tmp_or_bad = 1; + }; + + # $is_tmp_or_bad can only be trusted when we are operating on + # the local repo, so guard for that here + if ($is_tmp_or_bad && !defined $opts{from}) { + say " looks like stale tmp or bad file, with key " + . $unused_file->{key}; + } else { + # We need the RUN here to avoid special postprocessing but + # also to get the -c option passed -- unclear how to pass + # short options to git itself, not the 'log' subcommand, + # with Git::Wrapper except by using RUN (passing long + # options to git itself is easy, per Git::Wrapper docs) + my @log_lines = map { s/^/ /r } $self->git->RUN( + "-c", + "diff.renameLimit=3000", + "log", + { + stat => 1, + no_textconv => 1 + }, + "-3", + "--color=always", + "-S", + $unused_file->{key}); + + if ($opts{interactive}) { + # truncate log output if necessary to ensure user's + # terminal does not scroll + my (undef, $height, undef, undef) = GetTerminalSize(); + splice @log_lines, (($height - 5) - @log_lines) + if @log_lines > ($height - 5); + } say ""; - foreach my $unused_file (@unused_files) { - say_bold("unused file #" . $unused_file->{number} . ":"); - my $is_tmp_or_bad = 0; - my $content_location; - try { - ($content_location) = - $self->git->annex("contentlocation", $unused_file->{key}); - } catch { - $is_tmp_or_bad = 1; - }; - if ($is_tmp_or_bad) { - say " looks like stale tmp or bad file, with key " - . $unused_file->{key}; - } else { - # We need the RUN here to avoid special postprocessing but - # also to get the -c option passed -- unclear how to pass - # short options to git itself, not the 'log' subcommand, - # with Git::Wrapper except by using RUN (passing long - # options to git itself is easy, per Git::Wrapper docs) - my @log_lines = map { s/^/ /r } - $self->git->RUN("-c", "diff.renameLimit=3000", "log", - { - stat => 1, no_textconv => 1 }, - "-3", "--color=always", - "-S", $unused_file->{key}); - - say " " . $content_location; - say ""; - say for @log_lines; + say for @log_lines; + if ($opts{interactive}) { + my $response; + while (1) { + $response + = lc(prompt("Drop this unused file? (y/n/o)")); + if ($response eq 'y') { + push @to_drop, $unused_file->{number}; + last; + } elsif ($response eq 'n') { + last; + } elsif ($response eq 'o') { + system('xdg-open', $content_location); + } else { + say "invalid response"; + } } - say ""; } - $issues = 1; } + say ""; } - return $issues; + $self->git->annex("dropunused", \%dropunused_args, @to_drop) if @to_drop; + # return boolean value representing whether or not there are any + # unused files left after this run. note in non-interactive mode + # @to_drop will be empty so will always return 1 if we got this + # far in the subroutine + return @to_drop != @unused_files; } 1; |