Last active
October 21, 2025 12:51
-
-
Save pamaury/a7b49b81632a2234f3a0d283d6f060c6 to your computer and use it in GitHub Desktop.
Tool to compare PR history accross rebase
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/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