bin/diff-fancy.rb in git_helpers-0.1.0 vs bin/diff-fancy.rb in git_helpers-0.2
- old
+ new
@@ -1,699 +1 @@
-#!/usr/bin/env ruby
-# Inspired by diff-so-fancy; wrapper around diff-highlight
-# https://github.com/stevemao/diff-so-fancy
-# [commit: 0ea7c129420c57ec0384a704325e27c41f8f450d,
-# last commit checked: 3adf0114da99643ec53a16253a3d6f42390e4c19 (2017-04-04)]
-#TODO: use git-config
-#TODO: work with 'git log -p --graph'
-
-require "simplecolor"
-SimpleColor.mix_in_string
-begin
- require "shell_helpers"
-rescue LoadError
-end
-
-class GitDiff
- def self.output(gdiff, **opts)
- if gdiff.respond_to?(:each_line)
- enum=gdiff.each_line
- else
- enum=gdiff.each
- end
- self.new(enum, **opts).output
- end
-
- attr_reader :output
- include Enumerable
- NoNewLine="\\ No newline at end of file\n"
-
- def initialize(diff,**opts)
- @diff=diff #Assume diff is a line iterator ['gitdiff'.each_line]
- @current=0
- @mode=:unknown
- @opts=opts
- @opts[:color]=@opts.fetch(:color,true)
- #modes:
- #- unknown (temp mode)
- #- commit
- #- meta
- #- submodule_header
- #- submodule
- #- diff_header
- #- hunk
- @colors={meta: [:bold]}
- end
-
- def output_line(l)
- @output << l.chomp+"\n"
- end
- def output_lines(lines)
- lines.each {|l| output_line l}
- end
- def output
- each {|l| puts l}
- end
-
- def next_mode(nmode)
- @next_mode=nmode
- end
- def update_mode
- @start_mode=false
- @next_mode && change_mode(@next_mode)
- @next_mode=nil
- end
- def change_mode(nmode)
- @start_mode=true
- send :"end_#{@mode}" unless @mode==:unknown
- @mode=nmode
- send :"new_#{@mode}" unless @mode==:unknown
- end
-
- def new_commit; @commit={}; end
- def end_commit; end
- def new_meta; end
- def end_meta; end
- def new_hunk; end
- def end_hunk; end
- def new_submodule_header; @submodule={}; end
- def end_submodule_header; end
- def new_submodule; end
- def end_submodule; end
- def new_diff_header; @file={mode: :modify} end
- def end_diff_header; end
-
- def detect_new_diff_header
- @line =~ /^diff\s/
- end
- def detect_end_diff_header
- @line =~ /^\+\+\+\s/
- end
-
- def detect_new_hunk
- @line.match(/^@@+\s.*\s@@/)
- end
- def detect_end_hunk
- @hunk[:lines_seen].each_with_index.all? { |v,i| v==@hunk[:lines][i].first }
- end
-
- def handle_meta
- handle_line
- end
-
- def parse_hunk_header
- m=@line.match(/^@@+\s(.*)\s@@\s*(.*)/)
- hunks=m[1]
- @hunk={lines: []}
- @hunk[:header]=m[2]
- filenumber=0
- hunks.split.each do |hunk|
- hunkmode=hunk[0]
- hunk=hunk[1..-1]
- line,length=hunk.split(',').map(&:to_i)
- #handle hunks of the form @@ -1 +0,0 @@
- length,line=line,length unless length
- case hunkmode
- when '-'
- filenumber+=1
- @hunk[:lines][filenumber]=[length,line]
- when '+'
- @hunk[:lines][0]=[length,line]
- end
- end
- @hunk[:n]=@hunk[:lines].length
- @hunk[:lines_seen]=Array.new(@hunk[:n],0)
- end
-
- def handle_hunk
- if @start_mode
- parse_hunk_header
- else
- #'The 'No new line at end of file' is sort of part of the hunk, but
- #is not considerer in the hunkheader
- unless @line == NoNewLine
- #we need to wait for a NoNewLine to be sure we are at the end of the hunk
- return reparse(:unknown) if detect_end_hunk
- linemodes=@line[0...@hunk[:n]-1]
- newline=true
- #the line is on the new file unless there is a '-' somewhere
- if linemodes=~/-/
- newline=false
- else
- @hunk[:lines_seen][0]+=1
- end
- (1...@hunk[:n]).each do |i|
- linemode=linemodes[i-1]
- case linemode
- when '-'
- @hunk[:lines_seen][i]+=1
- when ' '
- @hunk[:lines_seen][i]+=1 if newline
- end
- end
- end
- end
- handle_line
- end
-
- def get_file_name(file)
- #remove prefix (todo handle the no-prefix option)
- file.gsub(/^[abciow12]\//,'')
- end
-
- def detect_filename
- if m=@line.match(/^---\s(.*)/)
- @file[:old_name]=get_file_name(m[1])
- return true
- end
- if m=@line.match(/^\+\+\+\s(.*)/)
- @file[:name]=get_file_name(m[1])
- return true
- end
- false
- end
-
- def detect_perm
- if m=@line.match(/^old mode\s+(.*)/)
- @file[:old_perm]=m[1]
- return true
- end
- if m=@line.match(/^new mode\s+(.*)/)
- @file[:new_perm]=m[1]
- return true
- end
- false
- end
-
- def detect_index
- if m=@line.match(/^index\s+(.*)\.\.(.*)/)
- @file[:oldhash]=m[1].split(',')
- @file[:hash],perm=m[2].split
- @file[:perm]||=perm
- return true
- end
- false
- end
-
- def detect_delete
- if m=@line.match(/^deleted file mode\s+(.*)/)
- @file[:old_perm]=m[1]
- @file[:mode]=:delete
- return true
- end
- false
- end
-
- def detect_newfile
- if m=@line.match(/^new file mode\s+(.*)/)
- @file[:new_perm]=m[1]
- @file[:mode]=:new
- return true
- end
- false
- end
-
- def detect_rename_copy
- if m=@line.match(/^similarity index\s+(.*)/)
- @file[:similarity]=m[1]
- return true
- end
- if m=@line.match(/^dissimilarity index\s+(.*)/)
- @file[:mode]=:rewrite
- @file[:dissimilarity]=m[1]
- return true
- end
- #if we have a rename with 100% similarity, there won't be any hunks so
- #we need to detect the filenames there
- if m=@line.match(/^(?:rename|copy) from\s+(.*)/)
- @file[:old_name]=m[1]
- end
- if m=@line.match(/^(?:rename|copy) to\s+(.*)/)
- @file[:name]=m[1]
- end
- if m=@line.match(/^rename\s+(.*)/)
- @file[:mode]=:rename
- return true
- end
- if m=@line.match(/^copy\s+(.*)/)
- @file[:mode]=:copy
- return true
- end
- false
- end
-
- def detect_diff_header
- if @start_mode
- if m=@line.chomp.match(/^diff\s--git\s(.*)\s(.*)/)
- @file[:old_name]=get_file_name(m[1])
- @file[:name]=get_file_name(m[2])
- elsif
- m=@line.match(/^diff\s--(?:cc|combined)\s(.*)/)
- @file[:name]=get_file_name(m[1])
- end
- true
- end
- end
-
- def handle_diff_header
- if detect_diff_header
- elsif detect_filename
- elsif detect_perm
- elsif detect_index
- elsif detect_delete
- elsif detect_newfile
- elsif detect_rename_copy
- else
- return reparse(:unknown)
- end
- next_mode(:unknown) if detect_end_diff_header
- handle_line
- end
-
- def detect_new_submodule_header
- if m=@line.chomp.match(/^Submodule\s(.*)\s(.*)/)
- subname=m[1];
- return not(@submodule && @submodule[:name]==subname)
- end
- false
- end
-
- def handle_submodule_header
- if m=@line.chomp.match(/^Submodule\s(\S*)\s(.*)/)
- subname=m[1]
- if @submodule[:name]
- #we may be dealing with a new submodule
- #require 'pry'; binding.pry
- return reparse(:submodule_header) if subname != @submodule[:name]
- else
- @submodule[:name]=m[1]
- end
- subinfo=m[2]
- if subinfo == "contains untracked content"
- @submodule[:untracked]=true
- elsif subinfo == "contains modified content"
- @submodule[:modified]=true
- else
- (@submodule[:info]||="") << subinfo
- next_mode(:submodule) if subinfo =~ /^.......\.\.\.?........*:$/
- end
- handle_line
- else
- return reparse(:unknown)
- end
- end
-
- def submodule_line
- @line=~/^ [><] /
- end
-
- def handle_submodule
- #we have lines indicating new commits
- #they always end by a new line except when followed by another submodule
- return reparse(:unknown) if !submodule_line
- handle_line
- end
-
- def detect_new_commit
- @line=~/^commit\b/
- end
-
- def handle_commit
- if m=@line.match(/^(\w+):\s(.*)/)
- @commit[m[1]]=m[2]
- handle_line
- else
- @start_mode ? handle_line : reparse(:unknown)
- end
- end
-
- def reparse(nmode)
- change_mode(nmode)
- parse_line
- end
-
- def handle_line
- end
-
-
- def parse_line
- case @mode
- when :unknown, :meta
- if detect_new_hunk
- return reparse(:hunk)
- elsif detect_new_diff_header
- return reparse(:diff_header)
- elsif detect_new_submodule_header
- return reparse(:submodule_header)
- elsif detect_new_commit
- return reparse(:commit)
- else
- change_mode(:meta) if @mode==:unknown
- handle_meta
- end
- when :commit
- handle_commit
- when :submodule_header
- handle_submodule_header
- when :submodule
- handle_submodule
- when :diff_header
- handle_diff_header
- #=> mode=unknown if we detect we are not a diff header anymore
- when :hunk
- handle_hunk
- #=> mode=unknown at end of hunk
- end
- end
-
- def prepare_new_line(line)
- @orig_line=line
- @line=@orig_line.uncolor
- update_mode
- end
-
- def parse
- Enumerator.new do |y|
- @output=y
- @diff.each do |line|
- prepare_new_line(line)
- parse_line
- yield if block_given?
- end
- change_mode(:unknown) #to trigger the last end_* hook
- end
- end
-
- def each(&b)
- parse.each(&b)
- end
-end
-
-class GitDiffDebug < GitDiff
- def initialize(*args,&b)
- super
- @cols=`tput cols`.to_i
- end
-
- def center(msg)
- msg.center(@cols,'─')
- end
-
- def handle_line
- super
- output_line "#{@mode}: #{@orig_line}"
- #p @hunk if @mode==:hunk
- end
-
- %i(commit meta diff_header hunk submodule_header submodule).each do |meth|
- define_method(:"new_#{meth}") do |*a,&b|
- super(*a,&b)
- output_line(center("New #{meth}"))
- end
- define_method(:"end_#{meth}") do |*a,&b|
- super(*a,&b)
- output_line(center("End #{meth}"))
- end
- end
-end
-
-#stolen from diff-highlight git contrib script
-class GitDiffHighlight < GitDiff
- def new_hunk
- super
- @accumulator=[[],[]]
- end
- def end_hunk
- super
- show_hunk
- end
-
- def highlight_pair(old,new)
- oldc=SimpleColor.color_entities(old).each_with_index
- newc=SimpleColor.color_entities(new).each_with_index
- seen_pm=false
- #find common prefix
- loop do
- a=oldc.grep {|c| ! SimpleColor.color?(c)}
- b=newc.grep {|c| ! SimpleColor.color?(c)}
- if !seen_pm and a=="-" and b=="+"
- seen_pm=true
- elsif a==b
- else
- last
- end
- #rescue StopIteration
- end
- end
-
- def show_hunk
- old,new=@accumulator
- if old.length != new.length
- output_lines(old+new)
- else
- newhunk=[]
- (0...old.length).each do |i|
- oldi,newi=highlight_pair(old[i],new[i])
- output_line oldi
- newhunk << newi
- end
- output_lines(newhunk)
- end
- end
-
- def handle_line
- if @mode == :hunk && @hunk[:n]==2
- linemode=@line[0]
- case linemode
- when "-"
- @accumulator[0] << @orig_line
- when "+"
- @accumulator[1] << @orig_line
- else
- show_hunk
- @accumulator=[[],[]]
- output_line @orig_line
- end
- else
- output_line @orig_line
- end
- end
-end
-
-class GitFancyDiff < GitDiff
-
- def initialize(*args,&b)
- super
- #when run inside a pager I get one more column so the line overflow
- #I don't know why
- cols=`tput cols`.to_i
- cols==0 && cols=80 #if TERM is not defined `tput cols` returns ''
- @cols=cols-1
- end
-
- def hline
- '─'*@cols
- end
- def hhline
- #'⬛'*@cols
- #"━"*@cols
- "═"*@cols
- end
-
- def short_perm_mode(m, prefix: '+')
- case m
- when "040000"
- prefix+"d" #directory
- when "100644"
- "" #file
- when "100755"
- prefix+"x" #executable
- when "120000"
- prefix+"l" #symlink
- when "160000"
- prefix+"g" #gitlink
- end
- end
- def perm_mode(m, prefix: ' ')
- case m
- when "040000"
- prefix+"directory"
- when "100644"
- "" #file
- when "100755"
- prefix+"executable"
- when "120000"
- prefix+"symlink"
- when "160000"
- prefix+"gitlink"
- end
- end
-
- def diff_header_summary
- r=case @file[:mode]
- when :modify
- "modified: #{@file[:name]}"
- when :rewrite
- "rewrote: #{@file[:name]} (dissimilarity: #{@file[:dissimilarity]})"
- when :new
- "added#{perm_mode(@file[:new_perm])}: #{@file[:name]}"
- when :delete
- "deleted#{perm_mode(@file[:old_perm])}: #{@file[:old_name]}"
- when :rename
- "renamed: #{@file[:old_name]} to #{@file[:name]} (similarity: #{@file[:similarity]})"
- when :copy
- "copied: #{@file[:old_name]} to #{@file[:name]} (similarity: #{@file[:similarity]})"
- end
- r<<" [#{short_perm_mode(@file[:old_perm],prefix:'-')}#{short_perm_mode(@file[:new_perm])}]" if @file[:old_perm] && @file[:new_perm]
- r
- end
-
- def meta_colorize(l)
- if @opts[:color]
- l.color(*@colors[:meta])
- else
- l
- end
- end
-
- def new_diff_header
- super
- output_line meta_colorize(hline)
- end
-
- def end_diff_header
- super
- output_line meta_colorize(diff_header_summary)
- output_line meta_colorize(hline)
- end
-
- def submodule_header_summary
- r="Submodule #{@submodule[:name]}"
- extra=[@submodule[:modified] && "modified", @submodule[:untracked] && "untracked"].compact.join("+")
- r<<" [#{extra}]" unless extra.empty?
- r << " #{@submodule[:info]}" if @submodule[:info]
- r
- end
-
- def new_submodule_header
- super
- output_line meta_colorize(hline)
- end
-
- def end_submodule_header
- super
- output_line meta_colorize(submodule_header_summary)
- output_line meta_colorize(hline)
- end
-
- def nonewline_clean
- @mode==:hunk && @file && (@file[:perm]=="120000" or @file[:old_perm]=="120000" or @file[:new_perm]=="120000") && @line==NoNewLine
- end
-
- def new_commit
- super
- output_line meta_colorize(hhline)
- end
- def end_commit
- super
- output_line meta_colorize(hhline)
- end
-
- def clean_hunk_col
- if @opts[:color] && @mode==:hunk && !@start_mode && @hunk[:n]==2
- bcolor,ecolor,line=SimpleColor.current_colors(@orig_line)
- m=line.scrub.match(/^([+-])?(.*)/)
- mode=m[1]
- cline=m[2]
- if mode && cline !~ /[^[:space:]]/ #detect blank line
- output_line SimpleColor.color(bcolor.to_s + (cline.empty? ? " ": cline)+ecolor.to_s,:inverse)
- else
- cline.sub!(/^\s/,'') unless mode #strip one blank character
- output_line bcolor.to_s+cline+ecolor.to_s
- end
- true
- end
- end
-
- def hunk_header
- if @mode==:hunk && @start_mode
- if @hunk[:lines][0][1] && @hunk[:lines][0][1] != 0
- header="#{@file[:name]}:#{@hunk[:lines][0][1]}"
- output_line @orig_line.sub(/(@@+\s)(.*)(\s@@+)/,"\\1#{header}\\3")
- end
- true
- end
- end
-
- def binary_file_differ
- @file and (@file[:mode]==:new && @line =~ %r{^Binary files /dev/null and ./#{@file[:name]} differ$} or
- @file[:mode]==:delete && @line =~ %r{^Binary files ./#{@file[:old_name]} and /dev/null differ$})
- end
-
- def handle_line
- super
- #:diff_header and submodule_header are handled at end_*
- case @mode
- when :meta
- if binary_file_differ
- else output_line @orig_line
- end
- when :hunk
- if hunk_header
- elsif nonewline_clean
- elsif clean_hunk_col
- else output_line @orig_line
- end
- when :submodule,:commit
- output_line @orig_line
- end
- end
-end
-
-if __FILE__ == $0
- require 'optparse'
-
- @opts={pager: true, diff_highlight: true, color: true, debug: false}
- optparse = OptionParser.new do |opt|
- opt.banner = "fancy git diff"
- opt.on("--[no-]pager", "launch the pager [true]") do |v|
- @opts[:pager]=v
- end
- opt.on("--[no-]highlight", "run the diff through diff-highlight [true]") do |v|
- @opts[:diff_highlight]=v
- end
- opt.on("--[no-]color", "color output [true]") do |v|
- @opts[:color]=v
- end
- opt.on("--raw", "Only parse diff headers") do |v|
- @opts[:color]=false
- @opts[:pager]=false
- @opts[:diff_highlight]=false
- end
- opt.on("--[no-]debug", "Debug mode") do |v|
- @opts[:debug]=v
- end
- end
- optparse.parse!
- @opts[:pager]=false unless Module.const_defined?('ShellHelpers')
- @opts[:pager] && ShellHelpers.run_pager #("--pattern '^(Date|added|deleted|modified): '")
-
- diff_highlight=ENV['DIFF_HIGHLIGHT']||"#{File.dirname(__FILE__)}/contrib/diff-highlight"
-
- args=ARGF
- if @opts[:debug]
- GitDiffDebug.new(args,**@opts).output
- elsif @opts[:diff_highlight]
- IO.popen(diff_highlight,'r+') do |f|
- Thread.new do
- args.each_line do |l|
- f.write(l)
- end
- f.close_write
- end
- GitFancyDiff.new(f,**@opts).output
- end
- else
- #diff=GitDiffHighlight.new(args,**@opts).parse
- GitFancyDiff.new(args,**@opts).output
- end
-end
\ No newline at end of file