Find file Copy path
Fetching contributors…
Cannot retrieve contributors at this time
executable file 308 lines (226 sloc) 8.32 KB
#!/usr/bin/env perl
### git-follow - Follow lifetime changes of a pathspec in Git.
### Copyright (C) 2017 Nickolas Burr <>
use 5.008;
use strict;
use warnings;
use lib "/usr/local/etc/git-follow/src";
use Cwd qw(getcwd);
use File::Basename;
use Getopt::Long;
use GitFollow::Core;
# @todo: Utilize $dispatch{"show_version"} instead.
# -------------------------------------------
# If --version (or -V) was given as an option,
# print the current release version and exit.
&show_version() if grep { $_ eq "--version" or $_ eq "-V" } @ARGV;
my ($pathspec, $refspec, @refs);
# Diff modes and their git-log(1) option counterparts.
my %diffopts = (
"inline" => "none",
"sxs" => "plain",
"colorsxs" => "color",
my %git_log = (
"exec" => "/usr/bin/git",
"pager_mode" => "--paginate",
"command" => "log",
my %git_log_options = (
"diff" => "--word-diff=%s",
"m" => "-m",
"follow" => "--follow",
"format" => "--format=%s",
"graph" => "--graph",
"patch" => "--patch-with-stat",
# follow.pager.disable configuration. Replace --paginate with --no-pager if set to true.
$git_log{"pager_mode"} = "--no-pager" if &has_config("pager", "disable") and &get_config("pager", "disable") eq "true";
# follow.diff.mode configuration.
if (&has_config("diff", "mode")) {
my $diffmode = &get_config("diff", "mode");
die sprintf("Invalid value '%s' specified for follow.diff.mode\n", $diffmode) unless grep { $_ eq $diffmode } keys %diffopts;
# Set corresponding --word-diff config value.
$git_log_options{"diff"} = sprintf($git_log_options{"diff"}, $diffopts{$diffmode});
# follow.log.format configuration.
$git_log_options{"format"} = sprintf($git_log_options{"format"}, (&has_config("log", "format") ? &get_config("log", "format") : $DEFAULT_LOG_FMT));
# Options and their conflicting counterparts.
my %copts = (
"no-merges" => [
"no-patch" => [
"no-renames" => [
"reverse" => [
# Default argument values for options that accept arguments.
my %dargs = (
"last" => 1,
"lines" => 1,
die "$USAGE_SYNOPSIS" unless @ARGV;
# Validate we're inside a Git repository.
die sprintf("%s\n%s", sprintf($INVALID_REPO_ERR, getcwd), $INVALID_REPO_HINT) unless &is_repo();
$pathspec = ($ARGV[$#ARGV] eq ".")
? &File::Basename::basename(getcwd)
: $ARGV[$#ARGV];
# @todo: Utilize $dispatch{"show_total"} instead.
# ------------------------------------------------
# If --total (or -T) was given as an option, print
# total number of revisions for pathspec and exit.
&show_total($pathspec) if grep { $_ eq "--total" or $_ eq "-T" } @ARGV;
my @apts = ();
my %dispatch = (
# Set alias, passthrough options and option arguments.
"set_apt_opt" => sub {
push @apts, &get_format_apt(($pathspec, @_));
"set_pager" => sub {
$git_log{"pager_mode"} = "--paginate";
"set_refspec" => sub {
&set_refspec((@_, \$refspec));
"set_unary_opt" => sub {
my $option = shift;
# Get formatted git-log(1) option from the unary option given.
$git_log_options{$option} = &get_format_apt($pathspec, $option);
# Remove any conflicting options from %git_log_options.
&rm_copts($option, \%copts, \%git_log_options);
"show_total" => sub {
"show_usage" => sub {
exit 0;
"show_version" => sub {
'branch|b=s{1,1}' => $dispatch{"set_refspec"},
'first|f' => $dispatch{"set_apt_opt"},
'func|F=s{1,1}' => $dispatch{"set_apt_opt"},
'last|l=i{0,1}' => $dispatch{"set_apt_opt"},
'lines|L=s{1,1}' => $dispatch{"set_apt_opt"},
'no-merges|M' => $dispatch{"set_unary_opt"},
'no-patch|N' => $dispatch{"set_unary_opt"},
'no-renames|O' => $dispatch{"set_unary_opt"},
'pager|p' => $dispatch{"set_pager"},
'pickaxe|P=s{1,1}' => $dispatch{"set_apt_opt"},
'range|r=s{1,1}' => $dispatch{"set_refspec"},
'reverse|R' => $dispatch{"set_unary_opt"},
'tag|t=s{1,1}' => $dispatch{"set_refspec"},
'total|T' => $dispatch{"show_total"},
'usage|help|h' => $dispatch{"show_usage"},
'version|V' => $dispatch{"show_version"},
) or &on_error();
# Set default ref if not given explicitly via --branch, --range, or --tag.
$refspec = "HEAD" unless defined $refspec;
# Attempt split at .. range delimiter.
@refs = split /\.{2}/, $refspec;
# Verify pathspec is valid given each ref in @refs.
foreach my $ref (@refs) {
die sprintf($INVALID_PATH_WITHIN_RANGE_ERR, $pathspec, $refspec) unless &is_pathspec($ref, $pathspec);
system $git_log{"exec"}, $git_log{"pager_mode"}, $git_log{"command"}, @apts, values %git_log_options, $refspec, "--", $pathspec;
=encoding UTF-8
=head1 NAME
git-follow - Follow lifetime changes of a pathspec in Git.
=head1 VERSION
version 1.1.5
Follow lifetime changes of a pathspec in Git. git-follow(1) makes analyzing changes of a pathspec trivial with robust options and simplified log output.
Configuration values set via git-config(1) can be used to customize the behavior of git-follow.
Diff mode. Choices include inline, sxs, and colorsxs. See --word-diff, --color-words, et al. of git-log(1).
Log format. See --format of git-log(1) for syntax.
Disable pager. Defaults to false. Set to true to disable pager. See --no-pager of git(1).
=head1 OPTIONS
--branch, -b <branchref>
Show commits specific to a branch.
--first, -f
Show first commit where Git initiated tracking of pathspec.
--func, -F <funcname>
Show commits which affected function <funcname> in pathspec. See -L of git-log(1).
--last, -l [<count>]
Show last <count> commits which affected pathspec. Omitting <count> defaults to last commit.
--lines, -L <start>[,<end>]
Show commits which affected lines <start> to <end> in pathspec. Omitting <end> defaults to EOF.
--no-merges, -M
Show commits which have a maximum of one parent. See --no-merges of git-log(1).
--no-patch, -N
Suppress diff output. See --no-patch of git-log(1).
--no-renames, -O
Disable rename detection. See --no-renames of git-log(1).
--pager, -p
Force pager when invoking git-log(1). Overrides follow.pager.disable config value.
--pickaxe, -P <string>
Show commits which change the number of occurrences of <string> in pathspec. See -S of git-log(1).
--range, -r <start>[,<end>]
Show commits in range <start> to <end>. Omitting <endref> defaults to HEAD. See git-revisions(1).
--reverse, -R
Show commits in reverse chronological order. See --walk-reflogs of git-log(1).
--tag, -t <tagref>
Show commits specific to a tag.
--total, -T
Show total number of commits for pathspec.
--version, -V
Show current release version.
=head1 NOTES
Like standard Git builtins, git-follow supports an optional pathspec delimiter [--] to help disambiguate options, option arguments, and refs from pathspecs.
Display commits on branch 'topic'
git follow --branch topic -- blame.c
Display first commit where Git initiated tracking
git follow --first -- branch.c
Display last 5 commits
git follow --last 5 -- Makefile
Display last commit where lines 5 through EOF were affected
git follow --last --lines 5 -- apply.c
Display last 3 commits where lines 10 through 15 were affected
git follow --last 3 --lines 10,15 -- bisect.c
Display commits where function `funcname' was affected
git follow --func funcname -- archive.c
Display commits in range from aa03428 to b354ef9
git follow --range aa03428,b354ef9 -- worktree.c
Display commits in range from v1.5.3 to v1.5.4
git follow --range v1.5.3,v1.5.4 -- apply.c
Display commits up to tag v1.5.3
git follow --tag v1.5.3 -- graph.c
Display total number of commits
git follow --total -- rebase.c
=head1 BUGS
=head1 AUTHOR
Written by Nickolas Burr <>
=head1 SEE ALSO
git(1), git-branch(1), git-check-ref-format(1), git-config(1), git-diff(1), git-log(1), git-remote(1), git-revisions(1), git-tag(1)
# vim: syntax=perl