Skip to content

Instantly share code, notes, and snippets.

@pamaury
Last active October 21, 2025 12:51
Show Gist options
  • Select an option

  • Save pamaury/a7b49b81632a2234f3a0d283d6f060c6 to your computer and use it in GitHub Desktop.

Select an option

Save pamaury/a7b49b81632a2234f3a0d283d6f060c6 to your computer and use it in GitHub Desktop.
Tool to compare PR history accross rebase
#!/usr/bin/env python3
"""
This scripts allows one to compute a diff between two pushes to a
Github pull request.
To use this tool, first you need to create a GitHub token. You only
need the minimal possible access (read access to public repositories).
Save it to a file, e.g. token.txt
You also need a copy of the repository of interest, and it needs to
contain the commits that you will want to compare. This can typically
be achieved by fetch the repository, or running `gh pr checkout`
on the PR to fetch the relevant branch. If you run this tool in a
different directory that the git repository, you can use
`--git /path/to/repo` to tell the tool where you checkout is.
Given a PR number <n>, you can first query the history of the PR by
running:
prdiff -t token.txt history <n>
This will display all pushes with numbers, e.g:
Event 0 on 2025-08-22 09:55:29+00:00: review_requested
Event 1 on 2025-08-22 09:55:29+00:00: review_requested
Event 2 on 2025-08-22 09:55:29+00:00: review_requested
Event 3 on 2025-08-22 09:55:29+00:00: review_requested
Event 4 on 2025-08-22 09:55:29+00:00: review_requested
Event 5 on 2025-08-22 10:43:08+00:00: head_ref_force_pushed
Event 6 on 2025-08-23 01:18:16+00:00: reviewed
Event 7 on 2025-08-25 13:17:36+00:00: head_ref_force_pushed
....
One you identify the two events <from> and <to> that you want to
compare, run:
prdiff -t token.txt interdiff --from <from> --to <to> <n>
You can also run it without `--from` and `--to` to compare the last
two pushes. If you need to run it on another repository, there are
more options available to select the owner/repo.
"""
import argparse
import requests
from pathlib import Path
import sys
import json
from datetime import datetime
import subprocess
def query_github(url, token_file):
token = token_file.read_text().strip(' \t\n')
headers = {
'Accept': 'application/vnd.github+json',
'Authorization': f"Bearer {token}",
'X-GitHub-Api-Version': '2022-11-28',
}
r = requests.get(url, headers = headers)
if r.status_code != 200:
print(f"Could not query URL {url}: {r.status_code}", file = sys.stderr)
print(r.text)
sys.exit(1)
return r.text
def get_history(args):
events = json.loads(query_github(
f"https://api.github.com/repos/{args.owner}/{args.repo}/issues/{args.pr}/timeline",
args.token,
))
# Create a fake force push event for the current state
current = json.loads(query_github(
f"https://api.github.com/repos/{args.owner}/{args.repo}/pulls/{args.pr}",
args.token,
))
events.append({
"id": current["id"],
"node_id": current["node_id"],
"url": current["url"],
"actor": current["user"],
"event": "head_ref_force_pushed",
"commit_id": current["head"]["sha"],
#"commit_url": "https://api.github.com/repos/pamaury/opentitan/commits/81a9b2f29313430b135d275f454c9897f1f8e6f6",
"created_at": current["created_at"],
"update_at_at": current["updated_at"],
})
return current, events
def show_history(args):
_, events = get_history(args)
for (i, event) in enumerate(events):
# print(json.dumps(event, indent=4))
evtdate = event.get("updated_at", None) or event["created_at"]
evtdate = datetime.fromisoformat(evtdate)
evt = event["event"]
print(f"Event {i} on {evtdate}: {evt}")
def find_from_to(events, _from, to):
if _from is not None and to is not None:
return _from, to
first_push = True
for i in range(len(events)-1,-1,-1):
if events[i]["event"] == "head_ref_force_pushed":
if first_push and to is None:
to = i
if not first_push and _from is None:
_from = i
break
first_push = False
return _from, to
def show_interdiff(args):
current, events = get_history(args)
evt_from, evt_to = find_from_to(events, args._from, args.to)
assert evt_from is not None and evt_to is not None
print(f"Showing interdiff between events {evt_from} and {evt_to}")
evt_from = events[evt_from]
evt_to = events[evt_to]
# print(json.dumps(current, indent = 4))
# print(json.dumps(evt_from, indent = 4))
# print(json.dumps(evt_to, indent = 4))
base = current["base"]["sha"]
old = evt_from["commit_id"]
new = evt_to["commit_id"]
subprocess.run([
"git",
"range-diff",
base,
old,
new
],
cwd = args.git,
)
def show_help(args):
args.parser.print_usage()
sys.exit(1)
def main():
parser = argparse.ArgumentParser(prog="prdiff")
parser.add_argument(
"--token",
"-t",
type=Path,
required=True,
help="Path to file storing the github token"
)
parser.add_argument(
"--git",
"-g",
type=Path,
default=".",
help="Path to the git repository (default is the current directory)"
)
parser.add_argument(
"--repo",
"-r",
type=str,
default="opentitan",
help="Github repository"
)
parser.add_argument(
"--owner",
"-o",
type=str,
default="lowRISC",
help="Github owner"
)
subparsers = parser.add_subparsers(help='subcommand help')
parser.set_defaults(func=show_help)
parser_history = subparsers.add_parser('history', help='show history of a PR')
parser_history.add_argument('pr', type=str, help='PR number')
parser_history.set_defaults(func=show_history)
parser_interdiff = subparsers.add_parser('interdiff', help='compare two versions of a PR')
parser_interdiff.add_argument('pr', type=str, help='PR number')
parser_interdiff.add_argument('--from', type=int, dest="_from", help='Event number to start from (default is before-last push)')
parser_interdiff.add_argument('--to', type=int, help='Event number to end at (default is last push)')
parser_interdiff.set_defaults(func=show_interdiff)
args = parser.parse_args()
args.parser = parser
args.func(args)
if __name__ == "__main__":
main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment