lib/git_fame/command.rb
# frozen_string_literal: true
require "active_support/core_ext/enumerable"
require "active_support/core_ext/object/blank"
require "tty-option"
require "tty-spinner"
module GitFame
class Command
include TTY::Option
using Extension
usage do
program "git"
command "fame"
desc "GitFame is a tool to generate a contributor list from git history"
example "Include commits made since 2010", "git fame --after 2010-01-01"
example "Include commits made before 2015", "git fame --before 2015-01-01"
example "Include commits made since 2010 and before 2015", "git fame --after 2010-01-01 --before 2015-01-01"
example "Only changes made to the main branch", "git fame --branch main"
example "Only ruby and javascript files", "git fame --extensions .rb .js"
example "Exclude spec files and the README", "git fame --exclude */**/*_spec.rb README.md"
example "Only spec files and markdown files", "git fame --include */**/*_spec.rb */**/*.md"
example "A parent directory of the current directory", "git fame ../other/git/repo"
end
option :log_level do
permit ["debug", "info", "warn", "error", "fatal"]
long "--log-level [LEVEL]"
desc "Log level"
end
option :exclude do
desc "Exclude files matching the given glob pattern"
long "--exclude [GLOB]"
arity zero_or_more
short "-E [BLOB]"
convert :list
end
option :include do
desc "Include files matching the given glob pattern"
long "--include [GLOB]"
arity zero_or_more
short "-I [BLOB]"
convert :list
end
option :extensions do
desc "File extensions to be included starting with a period"
arity zero_or_more
long "--extensions [EXT]"
short "-ex [EXT]"
convert :list
validate -> input do
input.match(/\.\w+/)
end
end
option :before do
desc "Only changes made after this date"
long "--before [DATE]"
short "-B [DATE]"
validate -> input do
Types::Params::DateTime.valid?(input)
end
end
option :after do
desc "Only changes made before this date"
long "--after [DATE]"
short "-A [DATE]"
validate -> input do
Types::Params::DateTime.valid?(input)
end
end
argument :path do
desc "Path or sub path to the git repository"
default { Dir.pwd }
optional
validate -> path do
File.directory?(path)
end
end
option :branch do
desc "Branch to be used as starting point"
long "--branch [NAME]"
default "HEAD"
end
flag :version do
desc "Current version"
long "--version"
short "-v"
end
flag :help do
desc "Print usage"
long "--help"
short "-h"
end
def self.call(argv = ARGV)
cmd = new
cmd.parse(argv, raise_on_parse_error: true)
cmd.run
rescue TTY::Option::InvalidParameter, TTY::Option::InvalidArgument => e
abort e.message
end
def run
if params[:help]
puts help
exit
end
if params[:version]
puts "git-fame v#{GitFame::VERSION}"
exit
end
thread = spinner.run do
Render.new(result: result, **options(:branch))
end
thread.value.call
rescue Dry::Struct::Error => e
abort e.message
rescue Interrupt
exit
end
private
def filter
Filter.new(**params.to_h.compact_blank.except(:branch))
end
def spinner
@spinner ||= TTY::Spinner.new("[:spinner] git-fame is crunching the numbers, hold on ...", interval: 1)
end
def repo
Rugged::Repository.discover(params[:path])
end
def collector
Collector.new(filter: filter, diff: diff, **options)
end
def diff
Diff.new(commit: commit, **options)
end
def options(*args)
params.to_h.only(*args, :log_level).compact_blank
end
def commit
repo.rev_parse(params[:branch])
end
def result
collector.call
end
end
end