git-branch-diffs 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121
  1. #!/bin/bash
  2. readonly BE_VERBOSE=0
  3. readonly USAGE="git-branch-diffs [-b <branch>] [-d] [-p <path>]* [<\"search-regex\">]
  4. Report which files differ per-branch compared to the master branch.
  5. OPTIONS:
  6. -b <branch>
  7. Consider only the specified branch.
  8. -d
  9. Display verbose diffs (like \`git diff\`)
  10. -p <path>
  11. Consider only the specified filesystem path.
  12. The argument may be absolute, or relative to the present working directory.
  13. This option may be given multiple times.
  14. <\"search-regex\">
  15. Filter returned results (like piping to \`grep\`).
  16. "
  17. readonly AMBIGUOUS_RX="warning: refname '.*' is ambiguous."
  18. ShowDiffs=0 # HhandleCliArgs()
  19. Branches=() # HandleCliArgs()
  20. Paths=() # HandleCliArgs()
  21. SearchRegex='' # HandleCliArgs()
  22. LOG() { (( BE_VERBOSE )) && echo -e "${@}" >&2 ; }
  23. HandleCliArgs()
  24. {
  25. ShowDiffs=0
  26. local filter_branch='*'
  27. while getopts 'b:dp:' arg
  28. do case "${arg}" in
  29. b) filter_branch=${OPTARG} ;;
  30. d) ShowDiffs=1 ;;
  31. p) Paths+=("${OPTARG}") ;;
  32. *) echo "${USAGE}" >&2 ; exit 1 ;;
  33. esac
  34. done
  35. shift $((OPTIND - 1))
  36. [[ "${filter_branch}" != master ]] || ! echo "branch may not be 'master'" || exit 1
  37. SearchRegex=$( [[ -n "$1" ]] && echo "$1" || echo '.*' )
  38. Branches=( $(git branch --list "${filter_branch}" | grep -v master | sed 's|^\* | |') )
  39. }
  40. IsUnambiguousRef() # (branch)
  41. {
  42. local branch=$1
  43. local ambiguous_warning="$(git log -1 ${branch} 2>&1 1>/dev/null | grep "${AMBIGUOUS_RX}")"
  44. [[ -n "${ambiguous_warning}" ]] && echo "${ambiguous_warning}" >&2
  45. [[ -z "${ambiguous_warning}" ]]
  46. }
  47. GitCommonAncestor() # ([-s] ref_a ref_b)
  48. {
  49. local is_short=$( [[ "$1" == '-s' ]] ; echo $(( ! $? )) ) ; (( is_short )) && shift ;
  50. local fmt=$( (( is_short )) && echo "%h" || echo "%h %ad %an [%G?] %s %d" )
  51. local ref_a=$1
  52. local ref_b=$( [[ -n "$2" ]] && echo $2 || echo HEAD )
  53. [[ -n "${ref_a}" ]] || ! echo "no ref specified" || return 1
  54. git log -n1 --format="${fmt}" --date=short $(git merge-base ${ref_a} ${ref_b})
  55. }
  56. Main()
  57. {
  58. local branch
  59. local common_ancestor
  60. local changed_files
  61. local dt
  62. local changed_file
  63. local change_msgs=()
  64. LOG "\n=== processing (${#Branches[*]}) branches ===\n"
  65. for branch in ${Branches[*]}
  66. do IsUnambiguousRef ${branch} || continue
  67. LOG "scanning branch: ${branch}"
  68. # detect diffs
  69. common_ancestor=$(GitCommonAncestor -s master ${branch})
  70. if (( ${#Paths[@]} ))
  71. then changed_files=( $(git diff --name-only ${common_ancestor} ${branch} -- "${Paths[@]}") )
  72. else changed_files=( $(git diff --name-only ${common_ancestor} ${branch} ) )
  73. fi
  74. (( ${#changed_files[@]} )) || continue
  75. # present results
  76. if (( ShowDiffs ))
  77. then dt=$(git log -1 --format='%cs' ${branch})
  78. echo '---' ; printf "[${branch}]: %s\n" "${dt}" ;
  79. git diff ${common_ancestor} ${branch} "${Paths[@]}"
  80. else for changed_file in "${changed_files[@]}"
  81. do if [[ "${changed_file}" =~ ${SearchRegex} ]]
  82. then dt=$(git log -1 --format='%cs' ${branch} -- "${changed_file}")
  83. change_msgs+=( "[${branch}]: ${dt} ${changed_file}" )
  84. fi
  85. done
  86. fi
  87. done
  88. LOG "\n\n=== per-branch file diffs ===\n"
  89. (( ${#change_msgs[@]} )) && printf "%s\n" "${change_msgs[@]}" || echo "none"
  90. }
  91. HandleCliArgs "$@"
  92. if (( ShowDiffs ))
  93. then Main "$@"
  94. else Main "$@" | column -t
  95. fi