1#!/usr/bin/env python3
2
3# Future improvements:
4# - link bugs and code references
5# - handle multiple bug references per line (currently only fetches one)
6# - fetch more than the first ~900 entries from Buganizer
7# - check if CLI tool is available + authenticated
8
9import pathlib, re, subprocess
10
11outfile = "closed_bugs.csv"
12
13print("Searching code for bug references")
14androidx_root = pathlib.Path(__file__).parent.parent.resolve()
15grep_cmd = subprocess.run(
16    ["egrep",
17
18     # -I excludes binary files
19     # -i case insensitive search
20     # -n include line numbers
21     # -r recursive
22     "-Iinr",
23
24     # regex for buganizer format ("b/123456789")
25     "b\/[[:digit:]]{8,9}",
26
27     # Files and directories to include and exclude
28     "--exclude-dir=.idea",
29     "--include=*.gradle",
30     "--include=*.java",
31     "--include=*.kt",
32     "--include=*.xml",
33
34     # Search all of the AndroidX repo checkout
35     f"{androidx_root}"
36     ],
37    capture_output=True,
38    text=True
39)
40raw_output_lines = grep_cmd.stdout.split("\n")
41
42print("Cleaning up search results")
43bug_dict = {} # mapping of bug id to list of filename + line number
44for line in raw_output_lines:
45    regex_result = re.search('b\/[0-9]{8,9}', line)
46    if regex_result is not None:
47        bug_id = regex_result.group(0).removeprefix("b/")
48        file = line.split(":")[0].removeprefix(str(androidx_root))
49        linenum = line.split(":")[1]
50
51        if bug_id in bug_dict:
52            matching_files = bug_dict[bug_id]
53        else:
54            matching_files = set()
55        matching_files.add(f"{file}:{linenum}")
56        bug_dict[bug_id] = matching_files
57print(f"Found {len(bug_dict)} bugs")
58
59# Create bug id query string.
60# The CLI tool fails if there are too many bugs (>900?); only use the first 900.
61bug_ids = list(bug_dict.keys())
62bug_ids.sort()
63joined_ids = "|".join(bug_ids[0:899])
64
65# Query buganizer to determine which of the given bugs are closed.
66# Store the issue, reporter, and assignee of the matching [closed] bugs.
67print("Querying Buganizer to find how many of these bugs are resolved")
68bugged_cmd = subprocess.run(
69    ["bugged", "search", f"id:({joined_ids})", "status:closed", "--columns=issue,reporter,assignee"],
70    capture_output=True,
71    text=True  # capture output as String instead of byte sequence
72)
73closed_bug_list = bugged_cmd.stdout.split("\n")
74
75# Remove header and trailing rows of Buganizer query result
76closed_bug_list.pop(0)
77closed_bug_list.pop()
78print(f"{len(closed_bug_list)} have been resolved")
79
80# Combine buganizer results with file search results and write to CSV
81csv_str = "bug_id,reporter,assignee,files\n"
82for line in closed_bug_list:
83    elements = re.split(" +", line)
84    bug_id = elements[0]
85    reporter = elements[1]
86    assignee = elements[2]
87    matching_files = bug_dict[bug_id]
88    line_str = f"b/{bug_id},{reporter},{assignee},"
89
90    # The list of matching file(s) are enclosed in double quotes to preserve \n in the csv
91    line_str += ("\"" + "\n".join(matching_files) + "\"")
92
93    csv_str += line_str + "\n"
94
95print(csv_str, file=open(outfile, 'w'))
96print(f"Wrote results to {outfile}")