1f117f0a7SLouis Dionne#!/usr/bin/env python3 2f117f0a7SLouis Dionne# ===----------------------------------------------------------------------===## 3f117f0a7SLouis Dionne# 4f117f0a7SLouis Dionne# Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions. 5f117f0a7SLouis Dionne# See https://llvm.org/LICENSE.txt for license information. 6f117f0a7SLouis Dionne# SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 7f117f0a7SLouis Dionne# 8f117f0a7SLouis Dionne# ===----------------------------------------------------------------------===## 9f117f0a7SLouis Dionne 10f117f0a7SLouis Dionnefrom typing import List, Dict, Tuple, Optional 11c2cac69dSLouis Dionneimport copy 12f117f0a7SLouis Dionneimport csv 13f117f0a7SLouis Dionneimport itertools 14f117f0a7SLouis Dionneimport json 15f117f0a7SLouis Dionneimport os 16f117f0a7SLouis Dionneimport pathlib 17f117f0a7SLouis Dionneimport re 18f117f0a7SLouis Dionneimport subprocess 19f117f0a7SLouis Dionne 20f117f0a7SLouis Dionne# Number of the 'Libc++ Standards Conformance' project on Github 21f117f0a7SLouis DionneLIBCXX_CONFORMANCE_PROJECT = '31' 22f117f0a7SLouis Dionne 23c2cac69dSLouis Dionnedef extract_between_markers(text: str, begin_marker: str, end_marker: str) -> Optional[str]: 24c2cac69dSLouis Dionne """ 25c2cac69dSLouis Dionne Given a string containing special markers, extract everything located beetwen these markers. 26c2cac69dSLouis Dionne 27c2cac69dSLouis Dionne If the beginning marker is not found, None is returned. If the beginning marker is found but 28c2cac69dSLouis Dionne there is no end marker, it is an error (this is done to avoid silently accepting inputs that 29c2cac69dSLouis Dionne are erroneous by mistake). 30c2cac69dSLouis Dionne """ 31c2cac69dSLouis Dionne start = text.find(begin_marker) 32c2cac69dSLouis Dionne if start == -1: 33c2cac69dSLouis Dionne return None 34c2cac69dSLouis Dionne 35c2cac69dSLouis Dionne start += len(begin_marker) # skip the marker itself 36c2cac69dSLouis Dionne end = text.find(end_marker, start) 37c2cac69dSLouis Dionne if end == -1: 38c2cac69dSLouis Dionne raise ArgumentError(f"Could not find end marker {end_marker} in: {text[start:]}") 39c2cac69dSLouis Dionne 40c2cac69dSLouis Dionne return text[start:end] 41c2cac69dSLouis Dionne 4284fa7b43SLouis Dionneclass PaperStatus: 4384fa7b43SLouis Dionne TODO = 1 4484fa7b43SLouis Dionne IN_PROGRESS = 2 4584fa7b43SLouis Dionne PARTIAL = 3 4684fa7b43SLouis Dionne DONE = 4 4784fa7b43SLouis Dionne NOTHING_TO_DO = 5 4884fa7b43SLouis Dionne 4984fa7b43SLouis Dionne _status: int 5084fa7b43SLouis Dionne 5184fa7b43SLouis Dionne _original: Optional[str] 5284fa7b43SLouis Dionne """ 5384fa7b43SLouis Dionne Optional string from which the paper status was created. This is used to carry additional 5484fa7b43SLouis Dionne information from CSV rows, like any notes associated to the status. 5584fa7b43SLouis Dionne """ 5684fa7b43SLouis Dionne 5784fa7b43SLouis Dionne def __init__(self, status: int, original: Optional[str] = None): 5884fa7b43SLouis Dionne self._status = status 5984fa7b43SLouis Dionne self._original = original 6084fa7b43SLouis Dionne 6184fa7b43SLouis Dionne def __eq__(self, other) -> bool: 6284fa7b43SLouis Dionne return self._status == other._status 6384fa7b43SLouis Dionne 6484fa7b43SLouis Dionne def __lt__(self, other) -> bool: 6584fa7b43SLouis Dionne relative_order = { 6684fa7b43SLouis Dionne PaperStatus.TODO: 0, 6784fa7b43SLouis Dionne PaperStatus.IN_PROGRESS: 1, 6884fa7b43SLouis Dionne PaperStatus.PARTIAL: 2, 6984fa7b43SLouis Dionne PaperStatus.DONE: 3, 7084fa7b43SLouis Dionne PaperStatus.NOTHING_TO_DO: 3, 7184fa7b43SLouis Dionne } 7284fa7b43SLouis Dionne return relative_order[self._status] < relative_order[other._status] 7384fa7b43SLouis Dionne 7484fa7b43SLouis Dionne @staticmethod 7584fa7b43SLouis Dionne def from_csv_entry(entry: str): 7684fa7b43SLouis Dionne """ 7784fa7b43SLouis Dionne Parse a paper status out of a CSV row entry. Entries can look like: 7884fa7b43SLouis Dionne - '' (an empty string, which means the paper is not done yet) 7984fa7b43SLouis Dionne - '|In Progress|' 8084fa7b43SLouis Dionne - '|Partial|' 8184fa7b43SLouis Dionne - '|Complete|' 8284fa7b43SLouis Dionne - '|Nothing To Do|' 8384fa7b43SLouis Dionne """ 8484fa7b43SLouis Dionne if entry == '': 8584fa7b43SLouis Dionne return PaperStatus(PaperStatus.TODO, entry) 86c2cac69dSLouis Dionne elif entry == '|In Progress|': 8784fa7b43SLouis Dionne return PaperStatus(PaperStatus.IN_PROGRESS, entry) 88c2cac69dSLouis Dionne elif entry == '|Partial|': 8984fa7b43SLouis Dionne return PaperStatus(PaperStatus.PARTIAL, entry) 90c2cac69dSLouis Dionne elif entry == '|Complete|': 9184fa7b43SLouis Dionne return PaperStatus(PaperStatus.DONE, entry) 92c2cac69dSLouis Dionne elif entry == '|Nothing To Do|': 9384fa7b43SLouis Dionne return PaperStatus(PaperStatus.NOTHING_TO_DO, entry) 9484fa7b43SLouis Dionne else: 9584fa7b43SLouis Dionne raise RuntimeError(f'Unexpected CSV entry for status: {entry}') 9684fa7b43SLouis Dionne 9784fa7b43SLouis Dionne @staticmethod 9884fa7b43SLouis Dionne def from_github_issue(issue: Dict): 9984fa7b43SLouis Dionne """ 10084fa7b43SLouis Dionne Parse a paper status out of a Github issue obtained from querying a Github project. 10184fa7b43SLouis Dionne """ 10284fa7b43SLouis Dionne if 'status' not in issue: 10384fa7b43SLouis Dionne return PaperStatus(PaperStatus.TODO) 10484fa7b43SLouis Dionne elif issue['status'] == 'Todo': 10584fa7b43SLouis Dionne return PaperStatus(PaperStatus.TODO) 10684fa7b43SLouis Dionne elif issue['status'] == 'In Progress': 10784fa7b43SLouis Dionne return PaperStatus(PaperStatus.IN_PROGRESS) 10884fa7b43SLouis Dionne elif issue['status'] == 'Partial': 10984fa7b43SLouis Dionne return PaperStatus(PaperStatus.PARTIAL) 11084fa7b43SLouis Dionne elif issue['status'] == 'Done': 11184fa7b43SLouis Dionne return PaperStatus(PaperStatus.DONE) 11284fa7b43SLouis Dionne elif issue['status'] == 'Nothing To Do': 11384fa7b43SLouis Dionne return PaperStatus(PaperStatus.NOTHING_TO_DO) 11484fa7b43SLouis Dionne else: 11584fa7b43SLouis Dionne raise RuntimeError(f"Received unrecognizable Github issue status: {issue['status']}") 11684fa7b43SLouis Dionne 11784fa7b43SLouis Dionne def to_csv_entry(self) -> str: 11884fa7b43SLouis Dionne """ 11984fa7b43SLouis Dionne Return the issue state formatted for a CSV entry. The status is formatted as '|Complete|', 12084fa7b43SLouis Dionne '|In Progress|', etc. 12184fa7b43SLouis Dionne """ 12284fa7b43SLouis Dionne mapping = { 12384fa7b43SLouis Dionne PaperStatus.TODO: '', 12484fa7b43SLouis Dionne PaperStatus.IN_PROGRESS: '|In Progress|', 12584fa7b43SLouis Dionne PaperStatus.PARTIAL: '|Partial|', 12684fa7b43SLouis Dionne PaperStatus.DONE: '|Complete|', 12784fa7b43SLouis Dionne PaperStatus.NOTHING_TO_DO: '|Nothing To Do|', 12884fa7b43SLouis Dionne } 12984fa7b43SLouis Dionne return self._original if self._original is not None else mapping[self._status] 13084fa7b43SLouis Dionne 131f117f0a7SLouis Dionneclass PaperInfo: 132f117f0a7SLouis Dionne paper_number: str 133f117f0a7SLouis Dionne """ 134f117f0a7SLouis Dionne Identifier for the paper or the LWG issue. This must be something like 'PnnnnRx', 'Nxxxxx' or 'LWGxxxxx'. 135f117f0a7SLouis Dionne """ 136f117f0a7SLouis Dionne 137f117f0a7SLouis Dionne paper_name: str 138f117f0a7SLouis Dionne """ 139f117f0a7SLouis Dionne Plain text string representing the name of the paper. 140f117f0a7SLouis Dionne """ 141f117f0a7SLouis Dionne 14284fa7b43SLouis Dionne status: PaperStatus 14384fa7b43SLouis Dionne """ 14484fa7b43SLouis Dionne Status of the paper/issue. This can be complete, in progress, partial, or done. 14584fa7b43SLouis Dionne """ 14684fa7b43SLouis Dionne 147f117f0a7SLouis Dionne meeting: Optional[str] 148f117f0a7SLouis Dionne """ 149f117f0a7SLouis Dionne Plain text string representing the meeting at which the paper/issue was voted. 150f117f0a7SLouis Dionne """ 151f117f0a7SLouis Dionne 152f117f0a7SLouis Dionne first_released_version: Optional[str] 153f117f0a7SLouis Dionne """ 154f117f0a7SLouis Dionne First version of LLVM in which this paper/issue was resolved. 155f117f0a7SLouis Dionne """ 156f117f0a7SLouis Dionne 157c2cac69dSLouis Dionne notes: Optional[str] 158f117f0a7SLouis Dionne """ 159c2cac69dSLouis Dionne Optional plain text string representing notes to associate to the paper. 160c2cac69dSLouis Dionne This is used to populate the "Notes" column in the CSV status pages. 161f117f0a7SLouis Dionne """ 162f117f0a7SLouis Dionne 163f117f0a7SLouis Dionne original: Optional[object] 164f117f0a7SLouis Dionne """ 165f117f0a7SLouis Dionne Object from which this PaperInfo originated. This is used to track the CSV row or Github issue that 166f117f0a7SLouis Dionne was used to generate this PaperInfo and is useful for error reporting purposes. 167f117f0a7SLouis Dionne """ 168f117f0a7SLouis Dionne 169f117f0a7SLouis Dionne def __init__(self, paper_number: str, paper_name: str, 17084fa7b43SLouis Dionne status: PaperStatus, 171f117f0a7SLouis Dionne meeting: Optional[str] = None, 172f117f0a7SLouis Dionne first_released_version: Optional[str] = None, 173c2cac69dSLouis Dionne notes: Optional[str] = None, 174f117f0a7SLouis Dionne original: Optional[object] = None): 175f117f0a7SLouis Dionne self.paper_number = paper_number 176f117f0a7SLouis Dionne self.paper_name = paper_name 177f117f0a7SLouis Dionne self.status = status 17884fa7b43SLouis Dionne self.meeting = meeting 179f117f0a7SLouis Dionne self.first_released_version = first_released_version 180c2cac69dSLouis Dionne self.notes = notes 181f117f0a7SLouis Dionne self.original = original 182f117f0a7SLouis Dionne 183f117f0a7SLouis Dionne def for_printing(self) -> Tuple[str, str, str, str, str, str]: 184f117f0a7SLouis Dionne return ( 185f117f0a7SLouis Dionne f'`{self.paper_number} <https://wg21.link/{self.paper_number}>`__', 186f117f0a7SLouis Dionne self.paper_name, 187f117f0a7SLouis Dionne self.meeting if self.meeting is not None else '', 18884fa7b43SLouis Dionne self.status.to_csv_entry(), 189f117f0a7SLouis Dionne self.first_released_version if self.first_released_version is not None else '', 190c2cac69dSLouis Dionne self.notes if self.notes is not None else '', 191f117f0a7SLouis Dionne ) 192f117f0a7SLouis Dionne 193f117f0a7SLouis Dionne def __repr__(self) -> str: 194f117f0a7SLouis Dionne return repr(self.original) if self.original is not None else repr(self.for_printing()) 195f117f0a7SLouis Dionne 196f117f0a7SLouis Dionne @staticmethod 197f117f0a7SLouis Dionne def from_csv_row(row: Tuple[str, str, str, str, str, str]):# -> PaperInfo: 198f117f0a7SLouis Dionne """ 199f117f0a7SLouis Dionne Given a row from one of our status-tracking CSV files, create a PaperInfo object representing that row. 200f117f0a7SLouis Dionne """ 201f117f0a7SLouis Dionne # Extract the paper number from the first column 202f117f0a7SLouis Dionne match = re.search(r"((P[0-9R]+)|(LWG[0-9]+)|(N[0-9]+))\s+", row[0]) 203f117f0a7SLouis Dionne if match is None: 204f117f0a7SLouis Dionne raise RuntimeError(f"Can't parse paper/issue number out of row: {row}") 205f117f0a7SLouis Dionne 206f117f0a7SLouis Dionne return PaperInfo( 207f117f0a7SLouis Dionne paper_number=match.group(1), 208f117f0a7SLouis Dionne paper_name=row[1], 20984fa7b43SLouis Dionne status=PaperStatus.from_csv_entry(row[3]), 210f117f0a7SLouis Dionne meeting=row[2] or None, 211f117f0a7SLouis Dionne first_released_version=row[4] or None, 212c2cac69dSLouis Dionne notes=row[5] or None, 213f117f0a7SLouis Dionne original=row, 214f117f0a7SLouis Dionne ) 215f117f0a7SLouis Dionne 216f117f0a7SLouis Dionne @staticmethod 217f117f0a7SLouis Dionne def from_github_issue(issue: Dict):# -> PaperInfo: 218f117f0a7SLouis Dionne """ 219f117f0a7SLouis Dionne Create a PaperInfo object from the Github issue information obtained from querying a Github Project. 220f117f0a7SLouis Dionne """ 221f117f0a7SLouis Dionne # Extract the paper number from the issue title 222f117f0a7SLouis Dionne match = re.search(r"((P[0-9R]+)|(LWG[0-9]+)|(N[0-9]+)):", issue['title']) 223f117f0a7SLouis Dionne if match is None: 224f117f0a7SLouis Dionne raise RuntimeError(f"Issue doesn't have a title that we know how to parse: {issue}") 225f117f0a7SLouis Dionne paper = match.group(1) 226f117f0a7SLouis Dionne 227c2cac69dSLouis Dionne # Extract any notes from the Github issue and populate the RST notes with them 228c2cac69dSLouis Dionne issue_description = issue['content']['body'] 229c2cac69dSLouis Dionne notes = extract_between_markers(issue_description, 'BEGIN-RST-NOTES', 'END-RST-NOTES') 230c2cac69dSLouis Dionne notes = notes.strip() if notes is not None else notes 231f117f0a7SLouis Dionne 232f117f0a7SLouis Dionne return PaperInfo( 233f117f0a7SLouis Dionne paper_number=paper, 234f117f0a7SLouis Dionne paper_name=issue['title'], 23584fa7b43SLouis Dionne status=PaperStatus.from_github_issue(issue), 236f117f0a7SLouis Dionne meeting=issue.get('meeting Voted', None), 237f117f0a7SLouis Dionne first_released_version=None, # TODO 238c2cac69dSLouis Dionne notes=notes, 239f117f0a7SLouis Dionne original=issue, 240f117f0a7SLouis Dionne ) 241f117f0a7SLouis Dionne 242c2cac69dSLouis Dionnedef merge(paper: PaperInfo, gh: PaperInfo) -> PaperInfo: 243c2cac69dSLouis Dionne """ 244c2cac69dSLouis Dionne Merge a paper coming from a CSV row with a corresponding Github-tracked paper. 245c2cac69dSLouis Dionne 246c2cac69dSLouis Dionne If the CSV row has a status that is "less advanced" than the Github issue, simply update the CSV 247c2cac69dSLouis Dionne row with the newer status. Otherwise, report an error if they have a different status because 248c2cac69dSLouis Dionne something must be wrong. 249c2cac69dSLouis Dionne 2506b3b63cdSLouis Dionne We don't update issues from 'To Do' to 'In Progress', since that only creates churn and the 2516b3b63cdSLouis Dionne status files aim to document user-facing functionality in releases, for which 'In Progress' 2526b3b63cdSLouis Dionne is not useful. 2536b3b63cdSLouis Dionne 254c2cac69dSLouis Dionne In case we don't update the CSV row's status, we still take any updated notes coming 255c2cac69dSLouis Dionne from the Github issue. 256c2cac69dSLouis Dionne """ 2576b3b63cdSLouis Dionne if paper.status == PaperStatus(PaperStatus.TODO) and gh.status == PaperStatus(PaperStatus.IN_PROGRESS): 2586b3b63cdSLouis Dionne result = copy.deepcopy(paper) 2596b3b63cdSLouis Dionne result.notes = gh.notes 2606b3b63cdSLouis Dionne elif paper.status < gh.status: 2616b3b63cdSLouis Dionne result = copy.deepcopy(gh) 2626b3b63cdSLouis Dionne elif paper.status == gh.status: 2636b3b63cdSLouis Dionne result = copy.deepcopy(paper) 2646b3b63cdSLouis Dionne result.notes = gh.notes 265c2cac69dSLouis Dionne else: 2666b3b63cdSLouis Dionne print(f"We found a CSV row and a Github issue with different statuses:\nrow: {paper}\nGithub issue: {gh}") 2676b3b63cdSLouis Dionne result = copy.deepcopy(paper) 2686b3b63cdSLouis Dionne return result 269c2cac69dSLouis Dionne 270f117f0a7SLouis Dionnedef load_csv(file: pathlib.Path) -> List[Tuple]: 271f117f0a7SLouis Dionne rows = [] 272f117f0a7SLouis Dionne with open(file, newline='') as f: 273f117f0a7SLouis Dionne reader = csv.reader(f, delimiter=',') 274f117f0a7SLouis Dionne for row in reader: 275f117f0a7SLouis Dionne rows.append(row) 276f117f0a7SLouis Dionne return rows 277f117f0a7SLouis Dionne 278f117f0a7SLouis Dionnedef write_csv(output: pathlib.Path, rows: List[Tuple]): 279f117f0a7SLouis Dionne with open(output, 'w', newline='') as f: 280f117f0a7SLouis Dionne writer = csv.writer(f, quoting=csv.QUOTE_ALL, lineterminator='\n') 281f117f0a7SLouis Dionne for row in rows: 282f117f0a7SLouis Dionne writer.writerow(row) 283f117f0a7SLouis Dionne 284*1b03747eSLouis Dionnedef create_github_issue(paper: PaperInfo, labels: List[str]) -> None: 285*1b03747eSLouis Dionne """ 286*1b03747eSLouis Dionne Create a new Github issue representing the given PaperInfo. 287*1b03747eSLouis Dionne """ 288*1b03747eSLouis Dionne paper_name = paper.paper_name.replace('``', '`').replace('\\', '') 289*1b03747eSLouis Dionne 290*1b03747eSLouis Dionne create_cli = ['gh', 'issue', 'create', '--repo', 'llvm/llvm-project', 291*1b03747eSLouis Dionne '--title', f'{paper.paper_number}: {paper_name}', 292*1b03747eSLouis Dionne '--body', f'**Link:** https://wg21.link/{paper.paper_number}', 293*1b03747eSLouis Dionne '--project', 'libc++ Standards Conformance', 294*1b03747eSLouis Dionne '--label', 'libc++'] 295*1b03747eSLouis Dionne 296*1b03747eSLouis Dionne for label in labels: 297*1b03747eSLouis Dionne create_cli += ['--label', label] 298*1b03747eSLouis Dionne 299*1b03747eSLouis Dionne print("Do you want to create the following issue?") 300*1b03747eSLouis Dionne print(create_cli) 301*1b03747eSLouis Dionne answer = input("y/n: ") 302*1b03747eSLouis Dionne if answer == 'n': 303*1b03747eSLouis Dionne print("Not creating issue") 304*1b03747eSLouis Dionne return 305*1b03747eSLouis Dionne elif answer != 'y': 306*1b03747eSLouis Dionne print(f"Invalid answer {answer}, skipping") 307*1b03747eSLouis Dionne return 308*1b03747eSLouis Dionne 309*1b03747eSLouis Dionne print("Creating issue") 310*1b03747eSLouis Dionne issue_link = subprocess.check_output(create_cli).decode().strip() 311*1b03747eSLouis Dionne print(f"Created tracking issue for {paper.paper_number}: {issue_link}") 312*1b03747eSLouis Dionne 313*1b03747eSLouis Dionne # Retrieve the "Github project item ID" by re-adding the issue to the project again, 314*1b03747eSLouis Dionne # even though we created it inside the project in the first place. 315*1b03747eSLouis Dionne item_add_cli = ['gh', 'project', 'item-add', LIBCXX_CONFORMANCE_PROJECT, '--owner', 'llvm', '--url', issue_link, '--format', 'json'] 316*1b03747eSLouis Dionne item = json.loads(subprocess.check_output(item_add_cli).decode().strip()) 317*1b03747eSLouis Dionne 318*1b03747eSLouis Dionne # Then, adjust the 'Meeting Voted' field of that item. 319*1b03747eSLouis Dionne meeting_voted_cli = ['gh', 'project', 'item-edit', 320*1b03747eSLouis Dionne '--project-id', 'PVT_kwDOAQWwKc4AlOgt', 321*1b03747eSLouis Dionne '--field-id', 'PVTF_lADOAQWwKc4AlOgtzgdUEXI', '--text', paper.meeting, 322*1b03747eSLouis Dionne '--id', item['id']] 323*1b03747eSLouis Dionne subprocess.check_call(meeting_voted_cli) 324*1b03747eSLouis Dionne 325*1b03747eSLouis Dionne # And also adjust the 'Status' field of the item to 'To Do'. 326*1b03747eSLouis Dionne status_cli = ['gh', 'project', 'item-edit', 327*1b03747eSLouis Dionne '--project-id', 'PVT_kwDOAQWwKc4AlOgt', 328*1b03747eSLouis Dionne '--field-id', 'PVTSSF_lADOAQWwKc4AlOgtzgdUBak', '--single-select-option-id', 'f75ad846', 329*1b03747eSLouis Dionne '--id', item['id']] 330*1b03747eSLouis Dionne subprocess.check_call(status_cli) 331*1b03747eSLouis Dionne 332*1b03747eSLouis Dionnedef sync_csv(rows: List[Tuple], from_github: List[PaperInfo], create_new: bool, labels: List[str] = None) -> List[Tuple]: 333f117f0a7SLouis Dionne """ 334f117f0a7SLouis Dionne Given a list of CSV rows representing an existing status file and a list of PaperInfos representing 335f117f0a7SLouis Dionne up-to-date (but potentially incomplete) tracking information from Github, this function returns the 336f117f0a7SLouis Dionne new CSV rows synchronized with the up-to-date information. 337f117f0a7SLouis Dionne 338*1b03747eSLouis Dionne If `create_new` is True and a paper from the CSV file is not tracked on Github yet, this also prompts 339*1b03747eSLouis Dionne to create a new issue on Github for tracking it. In that case the created issue is tagged with the 340*1b03747eSLouis Dionne provided labels. 341*1b03747eSLouis Dionne 342f117f0a7SLouis Dionne Note that this only tracks changes from 'not implemented' issues to 'implemented'. If an up-to-date 343f117f0a7SLouis Dionne PaperInfo reports that a paper is not implemented but the existing CSV rows report it as implemented, 344f117f0a7SLouis Dionne it is an error (i.e. the result is not a CSV row where the paper is *not* implemented). 345f117f0a7SLouis Dionne """ 346f117f0a7SLouis Dionne results = [rows[0]] # Start with the header 347f117f0a7SLouis Dionne for row in rows[1:]: # Skip the header 348f117f0a7SLouis Dionne # If the row contains empty entries, this is a "separator row" between meetings. 349f117f0a7SLouis Dionne # Preserve it as-is. 350f117f0a7SLouis Dionne if row[0] == "": 351f117f0a7SLouis Dionne results.append(row) 352f117f0a7SLouis Dionne continue 353f117f0a7SLouis Dionne 354f117f0a7SLouis Dionne paper = PaperInfo.from_csv_row(row) 355f117f0a7SLouis Dionne 35684fa7b43SLouis Dionne # Find any Github issues tracking this paper. Each row must have one and exactly one Github 35784fa7b43SLouis Dionne # issue tracking it, which we validate below. 358f117f0a7SLouis Dionne tracking = [gh for gh in from_github if paper.paper_number == gh.paper_number] 359f117f0a7SLouis Dionne 360*1b03747eSLouis Dionne # If there's more than one tracking issue, something is weird. 361*1b03747eSLouis Dionne if len(tracking) > 1: 362*1b03747eSLouis Dionne print(f"Found a row with more than one tracking issue: {row}\ntracked by: {tracking}") 36384fa7b43SLouis Dionne results.append(row) 36484fa7b43SLouis Dionne continue 365f117f0a7SLouis Dionne 366*1b03747eSLouis Dionne # If there is no tracking issue for that row and we are creating new issues, do that. 367*1b03747eSLouis Dionne # Otherwise just log that we're missing an issue. 368*1b03747eSLouis Dionne if len(tracking) == 0: 369*1b03747eSLouis Dionne if create_new: 370*1b03747eSLouis Dionne assert labels is not None, "Missing labels when creating new Github issues" 371*1b03747eSLouis Dionne create_github_issue(paper, labels=labels) 372*1b03747eSLouis Dionne else: 373*1b03747eSLouis Dionne print(f"Can't find any Github issue for CSV row: {row}") 37484fa7b43SLouis Dionne results.append(row) 37584fa7b43SLouis Dionne continue 376f117f0a7SLouis Dionne 377c2cac69dSLouis Dionne results.append(merge(paper, tracking[0]).for_printing()) 378f117f0a7SLouis Dionne 379f117f0a7SLouis Dionne return results 380f117f0a7SLouis Dionne 381*1b03747eSLouis DionneCSV_FILES_TO_SYNC = { 382*1b03747eSLouis Dionne 'Cxx17Issues.csv': ['c++17', 'lwg-issue'], 383*1b03747eSLouis Dionne 'Cxx17Papers.csv': ['c++17', 'wg21 paper'], 384*1b03747eSLouis Dionne 'Cxx20Issues.csv': ['c++20', 'lwg-issue'], 385*1b03747eSLouis Dionne 'Cxx20Papers.csv': ['c++20', 'wg21 paper'], 386*1b03747eSLouis Dionne 'Cxx23Issues.csv': ['c++23', 'lwg-issue'], 387*1b03747eSLouis Dionne 'Cxx23Papers.csv': ['c++23', 'wg21 paper'], 388*1b03747eSLouis Dionne 'Cxx2cIssues.csv': ['c++26', 'lwg-issue'], 389*1b03747eSLouis Dionne 'Cxx2cPapers.csv': ['c++26', 'wg21 paper'], 390*1b03747eSLouis Dionne} 391f117f0a7SLouis Dionne 392*1b03747eSLouis Dionnedef main(argv): 393*1b03747eSLouis Dionne import argparse 394*1b03747eSLouis Dionne parser = argparse.ArgumentParser(prog='synchronize-status-files', 395*1b03747eSLouis Dionne description='Synchronize the libc++ conformance status files with Github issues') 396*1b03747eSLouis Dionne parser.add_argument('--validate-only', action='store_true', 397*1b03747eSLouis Dionne help="Only perform the data validation of CSV files.") 398*1b03747eSLouis Dionne parser.add_argument('--create-new', action='store_true', 399*1b03747eSLouis Dionne help="Create new Github issues for CSV rows that do not correspond to any existing Github issue.") 400*1b03747eSLouis Dionne parser.add_argument('--load-github-from', type=str, 401*1b03747eSLouis Dionne help="A json file to load the Github project information from instead of querying the API. This is useful for testing to avoid rate limiting.") 402*1b03747eSLouis Dionne args = parser.parse_args(argv) 403*1b03747eSLouis Dionne 404f117f0a7SLouis Dionne libcxx_root = pathlib.Path(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) 405f117f0a7SLouis Dionne 406*1b03747eSLouis Dionne # Perform data validation for all the CSV files. 407*1b03747eSLouis Dionne print("Performing data validation of the CSV files") 408*1b03747eSLouis Dionne for filename in CSV_FILES_TO_SYNC: 409*1b03747eSLouis Dionne csv = load_csv(libcxx_root / 'docs' / 'Status' / filename) 410*1b03747eSLouis Dionne for row in csv[1:]: # Skip the header 411*1b03747eSLouis Dionne if row[0] != "": # Skip separator rows 412*1b03747eSLouis Dionne PaperInfo.from_csv_row(row) 413*1b03747eSLouis Dionne 414*1b03747eSLouis Dionne if args.validate_only: 415*1b03747eSLouis Dionne return 416*1b03747eSLouis Dionne 417*1b03747eSLouis Dionne # Load all the Github issues tracking papers from Github. 418*1b03747eSLouis Dionne if args.load_github_from: 419*1b03747eSLouis Dionne print(f"Loading all issues from {args.load_github_from}") 420*1b03747eSLouis Dionne with open(args.load_github_from, 'r') as f: 421*1b03747eSLouis Dionne project_info = json.load(f) 422*1b03747eSLouis Dionne else: 423f117f0a7SLouis Dionne print("Loading all issues from Github") 424f117f0a7SLouis Dionne gh_command_line = ['gh', 'project', 'item-list', LIBCXX_CONFORMANCE_PROJECT, '--owner', 'llvm', '--format', 'json', '--limit', '9999999'] 425f117f0a7SLouis Dionne project_info = json.loads(subprocess.check_output(gh_command_line)) 426f117f0a7SLouis Dionne from_github = [PaperInfo.from_github_issue(i) for i in project_info['items']] 427f117f0a7SLouis Dionne 428*1b03747eSLouis Dionne # Synchronize CSV files with the Github issues. 429*1b03747eSLouis Dionne for (filename, labels) in CSV_FILES_TO_SYNC.items(): 430f117f0a7SLouis Dionne print(f"Synchronizing {filename} with Github issues") 431f117f0a7SLouis Dionne file = libcxx_root / 'docs' / 'Status' / filename 432f117f0a7SLouis Dionne csv = load_csv(file) 433*1b03747eSLouis Dionne synced = sync_csv(csv, from_github, create_new=args.create_new, labels=labels) 434f117f0a7SLouis Dionne write_csv(file, synced) 435f117f0a7SLouis Dionne 436f117f0a7SLouis Dionneif __name__ == '__main__': 437*1b03747eSLouis Dionne import sys 438*1b03747eSLouis Dionne main(sys.argv[1:]) 439