xref: /llvm-project/libcxx/utils/synchronize_csv_status_files.py (revision 1b03747ed85cd4a6573b728674e88f4bd3fa844d)
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