package Local::MrRepo::Repo::Git; # Copyright (C) 2019, 2023 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 . use 5.018; use strict; use warnings; use lib "$ENV{HOME}/src/dotfiles/perl5"; use parent 'Local::MrRepo::Repo'; use Exporter 'import'; use File::Spec::Functions qw(rel2abs); use Git::Wrapper; use Local::ScriptStatus; use Local::Interactive qw(get_ack); use Local::Util::Git qw(unpushed_tags); use Try::Tiny; our @EXPORT_OK = (); # constructor sub new { my ($class, $toplevel) = @_; bless { toplevel => rel2abs($toplevel), updated => 0, git => Git::Wrapper->new(rel2abs($toplevel)), }, $class; } # attributes sub git { return shift->{git} } sub config_bool { my ($self, $key, $default) = @_; my @args = qw(--get --type=bool); push @args, "--default", $default if $default; my ($out) = $self->git->config(@args, $key); $out eq "true" ? 1 : 0 } # public methods # could make this runnable from a ~/bin/git-reviewrepo script or # something. Initialise a MrRepo::Repo::Git object in that script sub review { my $self = shift; my $issues = 0; # 1. Check for a detached HEAD which is not contained in any local # or remote ref, and might therefore contain useful work try { $self->git->symbolic_ref({ quiet => 1 }, "HEAD"); } catch { if ($_->status == 1) { # HEAD is detached my ($commit) = $self->git->rev_parse('HEAD^{commit}'); my @containing_refs = $self->git->for_each_ref({ contains => $commit }, "refs/heads", "refs/remotes"); if (@containing_refs == 0) { # HEAD contains work say_spaced_bullet("The HEAD is detached and contains commits not in any branch."); $issues = 1; } } }; # 2. Check for uncommitted changes. Note this won't work for # direct mode git annexes (but I no longer use any of those). # # check for uncommitted staged changes, unstaged changes to # tracked files and untracked files my @porcelain_status = $self->git->RUN("status", {porcelain => 1}); # check for stashes my @stash_list = $self->git->stash("list"); unless (@porcelain_status == 0 && @stash_list == 0) { my @status_lines = map { s/^/ /r } $self->git->RUN("-c", "color.status=always", "status", "--short"), # there doesn't appear to be a color output option for git stash "", $self->git->stash("list"); say_spaced_bullet("There is uncommitted work:"); say for @status_lines; $issues = 1; } # 3. Check for unpushed branches. The porcelain equivalent is # git --no-pager log --branches --not --remotes --simplify-by-decoration --decorate --oneline my @local_branches = grep { not m|\Arefs/heads/adjusted/| } $self->git->for_each_ref({format => '%(refname)'}, "refs/heads"); my @unpushed_branches; foreach my $branch (@local_branches) { my @containing_remotes = $self->git->for_each_ref({ contains => $branch }, "refs/remotes"); @containing_remotes = grep { not m|refs/remotes/dgit/| } @containing_remotes unless $branch =~ m|\Arefs/heads/dgit/|; @containing_remotes = grep { not m|refs/remotes/develacc/| } @containing_remotes; push @unpushed_branches, $branch unless @containing_remotes; } unless (@unpushed_branches == 0) { my @log_lines; push @log_lines, map { s/^/ /r } $self->git->RUN("log", "--decorate", "--oneline", "--color=always", "-1", $_) foreach @unpushed_branches; say_spaced_bullet ("There are local branches which are not pushed anywhere:"); say for @log_lines; say_bold ("\n Maybe you want to `git push-all' or `git branchmove' them to a remote."); $issues = 1; } # 4. Check for tags not pushed to any remote local $@; my @unpushed_tags; eval { @unpushed_tags = unpushed_tags($self->toplevel) }; if (my $exception = $@) { die $exception unless $exception =~ /failed to list git remote/; say_bullet("Warning: could not check for unpushed tags: $exception"); get_ack(); } unless (@unpushed_tags == 0) { say_bold("There are tags not pushed to any remote:"); print "\n"; print " $_\n" for @unpushed_tags; $issues = 1; } return $issues; } sub git_path { my $self = shift; my ($path) = $self->git->rev_parse({ git_path => 1 }, $_[0]); return rel2abs($path, $self->toplevel); } 1;