git-diffall — полный diff рабочего дерева ОДНИМ листингом

翻译

git-diffall.rb

#!/usr/bin/env ruby
# frozen_string_literal: true
# git-diffall — полный diff рабочего дерева ОДНИМ листингом.
# Поведение повторяет bash-версию; полная справка — константа GitDiffAll::HELP.
require 'open3'
module GitDiffAll
HELP = <<~TEXT
git-diffall — полный diff рабочего дерева ОДНИМ листингом:
* изменения tracked-файлов (только unstaged: working tree vs index)
* содержимое untracked-файлов в формате unified diff
Staged-изменения НЕ выводятся. Состояние репозитория НЕ меняется.
Использование: git diffall [--stat] [-- ...]
--stat только статистика (+/- по файлам)
... ограничить вывод этими путями
Весь вывод проходит через ОДИН pager (${PAGER:-less}); при перенаправлении
в файл/пайп — pager не запускается, отдаётся чистый текст.
TEXT
# Запрошена справка (-h/--help).
class HelpRequested < StandardError; end
# Не git-репозиторий.
class NotGitRepositoryError < StandardError; end
# Разбор аргументов командной строки.
class Options
attr_reader :paths
def initialize(stat: false, paths: [])
@stat = stat
@paths = paths
end
def stat?
@stat
end
def self.parse(argv)
stat = false
paths = []
args = argv.dup
until args.empty?
case args.shift
when '--stat' then stat = true
when '-h', '--help' then raise HelpRequested
when '--'
paths.concat(args)
args.clear
else
paths << args.first
end
end
new(stat: stat, paths: paths)
end
end
# Тонкая обёртка над git: все вызовы без собственного pager'а, цвет включается
# принудительно, только когда вывод идёт на терминал (через пайп git сам бы его
# выключил). Ненулевой exit status git-команд — не ошибка (у diff он означает
# «есть различия»), поэтому не проверяется, а stderr пробрасывается на терминал.
class Git
def initialize(force_color:, stderr: $stderr)
@force_color = force_color
@stderr = stderr
end
def repository?
system(*base_args, 'rev-parse', '--git-dir', out: File::NULL, err: File::NULL)
end
# Изменения tracked-файлов: working tree vs index (без staged).
def tracked_diff(paths)
run('diff', *selection(paths))
end
def tracked_stat(paths)
run('diff', '--stat', *selection(paths))
end
# Untracked-файлы (с исключением игнорируемых), NUL-разделитель —
# безопасен для любых имён, включая переводы строк.
def untracked_files(paths)
out, = Open3.capture3(*base_args, 'ls-files', '--others', '--exclude-standard',
'-z', *selection(paths))
out.split("\x0").reject(&:empty?)
end
# Diff одного untracked-файла против /dev/null (для git это «пустой файл»;
# File::NULL на Linux совпадает с '/dev/null').
def untracked_diff(path)
run('diff', '--no-index', File::NULL, path)
end
private
def base_args
args = ['git', '--no-pager']
args.push('-c', 'color.diff=always') if @force_color
args
end
def selection(paths)
paths.empty? ? [] : ['--', *paths]
end
def run(*)
stdout, stderr, = Open3.capture3(*base_args, *)
@stderr.write(stderr) unless stderr.empty?
stdout
end
end
# «Тело» вывода: секция по tracked-файлам + секции по untracked-файлам.
# Подклассы задают содержимое секций и разделитель (шаблонный метод).
class Body
def initialize(git:, paths:)
@git = git
@paths = paths
end
def render
([tracked_section] + untracked_sections).join(separator) + terminator
end
private
def separator
''
end
# Хвостовая строка всего вывода (пустая у DiffBody).
def terminator
''
end
def tracked_section
raise NotImplementedError
end
def untracked_sections
raise NotImplementedError
end
end
# Полный diff: git diff + по одному unified-diff на untracked-файл.
class DiffBody < Body
private
def tracked_section
@git.tracked_diff(@paths)
end
def untracked_sections
@git.untracked_files(@paths).map { |file| @git.untracked_diff(file) }
end
end
# Статистика: git diff --stat + счётчик строк по untracked-файлам.
# Секции — строки без хвостовых переводов строк: разделитель и терминатор
# задают «склейку» точно как в bash (printf с \n + echo).
class StatBody < Body
private
def separator
"\n"
end
def terminator
"\n"
end
def tracked_section
@git.tracked_stat(@paths)
end
def untracked_sections
@git.untracked_files(@paths).map do |file|
format(' %s | %d ++++', file: file, count: line_count(file))
end
end
def line_count(file)
File.foreach(file).count
end
end
# Один pager на весь вывод; при перенаправлении в файл/пайп не запускается.
module Pager
extend self
def interactive?
$stdout.tty?
end
def command
pager = ENV['GIT_PAGER'] || ENV['PAGER'] || 'less'
pager == 'less' ? 'less -FRX' : pager
end
def write(output)
IO.popen(command, 'w') { |io| io.write(output) }
rescue Errno::EPIPE, Errno::ENOENT, Errno::EACCES
# Pager закрылся раньше (q) или не найден — как bash-овское `|| true`,
# это не ошибка.
end
end
# Оркестрация: проверка репозитория, разбор аргументов, сбор вывода, pager.
class CLI
def initialize(argv, stdout: $stdout, stderr: $stderr)
@argv = argv
@stdout = stdout
@stderr = stderr
end
def run
git = Git.new(force_color: Pager.interactive?)
raise NotGitRepositoryError unless git.repository?
options = Options.parse(@argv)
body = if options.stat?
StatBody.new(git: git, paths: options.paths)
else
DiffBody.new(git: git, paths: options.paths)
end
if Pager.interactive?
Pager.write(body.render)
else
@stdout.write(body.render)
end
0
rescue HelpRequested
@stdout.write(HELP)
0
rescue NotGitRepositoryError
@stderr.puts('git-diffall: не git-репозиторий')
1
end
end
end
# Скрипт стоит вне Rails-приложения, поэтому exit здесь уместен.
exit GitDiffAll::CLI.new(ARGV).run # rubocop:disable Rails/Exit
添加评论
点赞收藏
点踩分享查看原文
评论
?
参与讨论