#!/usr/bin/env ruby require "shellwords" def error(msg, code) $stderr.puts msg; exit code end def git(*args) cmd = ["git", *args].map{|s| s.to_s}.shelljoin out = IO.popen(cmd, err: File::NULL, &:read) raise unless $?.success? out rescue "" end def resolve_merge(x) ps = git("rev-list","--parents","-n1",x).split return nil if ps.empty? || ps.size < 3 ps[0] end def find_by_ancestry(arg_commit, revset) git("rev-list","--merges",*revset).split.each do |m| line = git("rev-list","--parents","-n1",m).split next if line.size < 3 line[2..-1].each do |p| return m if system("git","merge-base","--is-ancestor",p,arg_commit) end end nil end def find_by_grep(needle, revset) pats = [ "Merge branch '#{needle}' ", "Merge branch '#{needle}'", "Merge remote-tracking branch 'origin/#{needle}'", "Merge branch \"#{needle}\"", "Merge branch #{needle}" ] pats.each do |pat| h = git("log",*revset,"--merges","--pretty=%H","--fixed-strings","--grep",pat,"-n1").lines.first&.strip return h unless h.to_s.empty? end nil end arg = ARGV[0] || "" tgt = ARGV[1] error("usage: #{$0} [target-branch]", 1) if arg.empty? merge = resolve_merge(arg) revset = if tgt && !tgt.empty? error("no such target: #{tgt}", 2) if git("rev-parse","--verify","#{tgt}^{commit}").empty? [tgt] else ["--all"] end if !merge arg_commit = git("rev-parse","--verify","#{arg}^{commit}").strip if !arg_commit.empty? merge = find_by_ancestry(arg_commit, revset) end merge ||= find_by_grep(arg, revset) end error("no merge commit found for '#{arg}' in #{revset.join(",")}", 3) if !merge || merge.empty? p1 = git("rev-parse","#{merge}^1").strip error("could not resolve first parent of #{merge}", 4) if p1.empty? ok = system("git","diff","--binary",p1,merge) exit(5) unless ok