summaryrefslogtreecommitdiff
path: root/scripts/check_mergability.sh
blob: ef99194feec992de284bfd5a933848982635f2c5 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
#!/bin/sh

# How to run:
# * Install the gh cli and run `gh login`: https://github.com/cli/cli/
# * install black isort usort pyupgrade and whatever other tools you want to
#   play with in your active virtualenv
# * move to a new folder for the script to work in: `mkdir pr_mergability && cd pr_mergability`
# * ../scripts/check_mergability.sh
#
# It'll clone the qutebrowser repo, fetch refs for all the open PRs, checkout
# a branch, run auto formatters, try to merge each PR, report back via CSV
# how badly each merge filed (via "number of conflicting lines").
#
# For details of what auto formatters are ran see the `tools` variable down
# near the bottom of the script.
#
# If you've checked out a branch and ran auto-formatters or whatever on it
# manually and just want the script to try to merge all PRs you can call it
# with the branch name and it'll do so. Remember to go back up to the work dir
# before calling the script.
#
# If it's been a few days and PRs have been opened or merged delete `prs.json`
# from the working dir to have them re-fetched on next run.
# If PRs have had updates pushed you'll have to update the refs yourself or
# nuke the whole clone in the work dir and let the script re-fetch them all.

# requires the github binary, authorized, to list open PRs.
command -v gh > /dev/null || {
  echo "Error: Install the github CLI, gh, make sure it is in PATH and authenticated."
  exit 1
}
# requires some formatting tools available. The are all installable via pip.
all_formatters="black isort usort pyupgrade"
for cmd in $all_formatters; do
  command -v $cmd >/dev/null || {
    echo "Error: Requires all these tools to be in PATH (install them with pip): $all_formatters"
    exit 1
  }
done

[ -e qutebrowser/app.py ] && {
  echo "don't run this from your qutebrowser checkout. Run it from a tmp dir, it'll checkout out a new copy to work on"
  exit 1
}
[ -d qutebrowser ] || {
  git clone git@github.com:qutebrowser/qutebrowser.git
  cd qutebrowser
  git config --local merge.conflictstyle merge
  git config --local rerere.enabled false
  cd -
}

[ -e prs.json ] || {
  # (re-)fetch list of open PRs. Pull refs for any new ones.
  # Resets master and qt6-v2 in case they have changed. Does not handle
  # fetching new changes for updated PRs.
  echo "fetching open PRs"
  gh -R qutebrowser/qutebrowser pr list -s open --json number,title,mergeable,updatedAt -L 100 > prs.json
  cd qutebrowser
  git fetch
  git checkout master && git pull
  git checkout qt6-v2 && git pull
  # this is slow for a fresh clone, idk how to fetch all pull/*/head refs at once
  jq -r '.[] | "\(.number) \(.updatedAt) \(.title)"' < ../prs.json | while read number updated title; do
    git describe pr/$number >/dev/null 2>&1 || git fetch origin refs/pull/$number/head:pr/$number
  done
  cd -
}

python3 <<"EOF"
import json
from collections import Counter
import rich

with open("prs.json") as f: prs=json.load(f)

rich.print(Counter([p['mergeable'] for p in prs]))
# Counter({'MERGEABLE': 29, 'CONFLICTING': 45})
EOF

summary () {
  # Summarize the accumulated report CSVs
  # Should be the last thing we do since it goes back up to the report dir
  cd - >/dev/null
  python3 <<"EOF"
import csv, glob

def read_csv(path):
    with open(path) as f:
        return list(csv.DictReader(f))

for report in sorted(glob.glob("report-*.csv")):
    rows = read_csv(report)
    succeeded = len([row for row in rows if row["state"] == "succeeded"])
    failed = len([row for row in rows if row["state"] == "failed"])
    print(f"{report} {succeeded=} {failed=}")
EOF
}

prompt_or_summary () {
    printf "$1 [Yn]: "
    read ans
    case "$ans" in
      [nN]*)
        summary
        exit 0
        ;;
      *) true;;
    esac
}

generate_report () {
  # checkout a branch, try to merge each of the open PRs, write the results to
  # a CSV file
  base="${1:-master}"
  quiet=$2

  git checkout -q $base

  report_file=../report-$base.csv
  [ -e $report_file ] && [ -z "$quiet" ] && {
    prompt_or_summary "$report_file exists, overwrite?"
  }

  echo "number,updated,title,state,clean,conflicting" > $report_file
  report () {
    echo "$1,$2,\"$3\",$4,$5,$6" >> $report_file
  }

  jq -r '.[] | "\(.number) \(.updatedAt) \(.title)"' < ../prs.json | while read number updated title; do
    [ -n "$quiet" ] || echo "trying pr/$number $updated $title"
    head_sha=$(git rev-parse HEAD)
    git merge -q --no-ff --no-edit pr/$number 2>&1 1>/dev/null | grep -v preimage
    if [ -e .git/MERGE_HEAD ] ;then
      # merge failed, clean lines staged and conflicting lines in working
      # tree
      merged_lines=$(git diff --cached --numstat | awk -F'	' '{sum+=$1;} END{print sum;}')
      conflicting_lines=$(git diff | sed -n -e '/<<<<<<< HEAD/,/=======$/p' -e '/=======$/,/>>>>>>> pr/p' | wc -l)
      conflicting_lines=$(($conflicting_lines-4)) # account for markers included in both sed expressions
      [ -n "$quiet" ] || echo "#$number failed merging merged_lines=$merged_lines conflicting_lines=$conflicting_lines"
      git merge --abort
      report $number $updated "$title" failed $merged_lines $conflicting_lines
    else
      [ -n "$quiet" ] || echo "#$number merged fine"
      #git show HEAD --oneline --stat
      git reset -q --hard $head_sha
      report $number $updated "$title" succeeded 0 0
    fi
  done
}

cd qutebrowser

# run as `$0 some-branch` to report on merging all open PRs to a branch you
# made yourself. Otherwise run without args to try with a bunch of builtin
# configurations.
if [ -n "$1" ] ;then
  generate_report "$1"
else
  usort () { env usort format "$@"; }
  pyupgrade () { git ls-files | grep -F .py | xargs pyupgrade --py37-plus; }
  clean_branches () {
    # only clean up tmp- branches in case I run it on my main qutebrowser
    # checkout by mistake :)
    git checkout master
    git reset --hard origin/master
    git branch -l | grep tmp- | grep -v detached | while read l; do git branch -qD $l ;done
  }

  # pre-defined auto-formatter configurations. Branches will be created as
  # needed.
  # format: branch tool1 tool2 ...
  tools="master true
  tmp-black black
  tmp-black_isort black isort
  tmp-black_usort black usort
  tmp-black_pyupgrade black pyupgrade
  tmp-black_isort_pyupgrade black isort pyupgrade
  tmp-black_isort_pyupgrade black usort pyupgrade
  qt6-v2 true"
  #tools="tmp-black_isort black isort
  #tmp-black_usort black usort"

  prompt_or_summary "Generate report for all tool configurations?"
  clean_branches

  echo "$tools" | while read branch cmds; do
    echo "$branch"
    git checkout -q "$branch" 2>/dev/null || git checkout -q -b "$branch" origin/master
    echo "$cmds" | tr ' ' '\n' | while read cmd; do
      $cmd qutebrowser tests
      git commit -am "$cmd"
    done
    generate_report "$branch" y
  done
fi

summary

# todo:
# * see if we can run formatters on PR branches before/while merging
# * do most stuff based off of qt6-v2 instead of master, not like most PRs
#   will be merged to pre-3.0 master anyway
# notes:
# after merging qt6-v2 would merging old PRs to old master then somehow merging
#   the PR merge commit up to the new master easier than rebasing the PR?
# there is a filter attribute you can use to re-write files before committing.
#   For this use case probably the same as rebase -i --exec then merge?
#   >See "Merging branches with differing checkin/checkout attributes" in gitattributes(5)