From be26367fc36acaaeb48079038f13d20ba44bc52c Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 11:58:52 -0400 Subject: [PATCH 01/18] Update [ghstack-poisoned] --- src/ghstack/github.py | 14 ++++ src/ghstack/github_fake.py | 154 ++++++++++++++++++++++++++++++++++--- src/ghstack/github_real.py | 135 +++++++++++++++++++++++++++++--- src/ghstack/shell.py | 17 +++- 4 files changed, 299 insertions(+), 21 deletions(-) diff --git a/src/ghstack/github.py b/src/ghstack/github.py index 31343ca..c135269 100644 --- a/src/ghstack/github.py +++ b/src/ghstack/github.py @@ -95,3 +95,17 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any: Returns: parsed JSON response """ pass + + @abstractmethod + async def arest(self, method: str, path: str, **kwargs: Any) -> Any: + """ + Send an async 'method' request to endpoint 'path'. + + Args: + method: 'GET', 'POST', etc. + path: relative URL path to access on endpoint + **kwargs: dictionary of JSON payload to send + + Returns: parsed JSON response + """ + pass diff --git a/src/ghstack/github_fake.py b/src/ghstack/github_fake.py index a54a3bb..e157445 100644 --- a/src/ghstack/github_fake.py +++ b/src/ghstack/github_fake.py @@ -1,8 +1,10 @@ #!/usr/bin/env python3 +import asyncio import dataclasses import os.path import re +import subprocess from dataclasses import dataclass from typing import Any, cast, Dict, List, NewType, Optional, Sequence @@ -168,16 +170,16 @@ def __init__(self, upstream_sh: Optional[ghstack.shell.Shell]) -> None: # operations depend on repository state (e.g., what # the headRef is at the time a PR is created), so # we need this information - self.upstream_sh.git("init", "--bare", "-b", "main") + self.upstream_sh.git("init", "--bare", "-b", "master") tree = self.upstream_sh.git("write-tree") commit = self.upstream_sh.git("commit-tree", tree, input="Initial commit") - self.upstream_sh.git("branch", "-f", "main", commit) + self.upstream_sh.git("branch", "-f", "master", commit) # We only update this when a PATCH changes the default # branch; hopefully that's fine? In any case, it should # work for now since currently we only ever access the name # of the default branch rather than other parts of its ref. - repo.defaultBranchRef = repo._make_ref(self, "main") + repo.defaultBranchRef = repo._make_ref(self, "master") @dataclass @@ -237,6 +239,32 @@ def _make_ref(self, state: GitHubState, refName: str) -> "Ref": ) return ref + async def _make_ref_async(self, state: GitHubState, refName: str) -> "Ref": + assert state.upstream_sh + proc = await asyncio.create_subprocess_exec( + "git", + "rev-parse", + refName, + cwd=state.upstream_sh.cwd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + out, _err = await proc.communicate() + if proc.returncode != 0: + raise RuntimeError(f"git rev-parse {refName} failed") + gitObject = GitObject( + id=state.next_id(), + oid=GitObjectID(out.decode(errors="backslashreplace").strip()), + _repository=self.id, + ) + ref = Ref( + id=state.next_id(), + name=refName, + _repository=self.id, + target=gitObject, + ) + return ref + @dataclass class GitObject(Node): @@ -347,17 +375,11 @@ def graphql(self, query: str, **kwargs: Any) -> Any: variable_values=kwargs, ) if r.errors: - # The GraphQL implementation loses all the stack traces!!! - # D: You can 'recover' them by deleting the - # 'except Exception as error' from GraphQL-core-next; need - # to file a bug report raise RuntimeError( "GraphQL query failed with errors:\n\n{}".format( "\n".join(str(e) for e in r.errors) ) ) - # The top-level object isn't indexable by strings, but - # everything underneath is, oddly enough return {"data": r.data} def push_hook(self, refNames: Sequence[str]) -> None: @@ -401,6 +423,36 @@ def _create_pull( "number": number, } + async def _create_pull_async( + self, owner: str, name: str, input: CreatePullRequestInput + ) -> CreatePullRequestPayload: + state = self.state + id = state.next_id() + repo = state.repository(owner, name) + number = state.next_pull_request_number(repo.id) + baseRef = None + headRef = None + if state.upstream_sh: + baseRef = await repo._make_ref_async(state, input["base"]) + headRef = await repo._make_ref_async(state, input["head"]) + pr = PullRequest( + id=id, + _repository=repo.id, + number=number, + closed=False, + url="https://github.com/{}/pull/{}".format(repo.nameWithOwner, number), + baseRef=baseRef, + baseRefName=input["base"], + headRef=headRef, + headRefName=input["head"], + title=input["title"], + body=input["body"], + ) + state.pull_requests[id] = pr + return { + "number": number, + } + # NB: This technically does have a payload, but we don't # use it so I didn't bother constructing it. def _update_pull( @@ -419,6 +471,20 @@ def _update_pull( if "body" in input and input["body"] is not None: pr.body = input["body"] + async def _update_pull_async( + self, owner: str, name: str, number: GitHubNumber, input: UpdatePullRequestInput + ) -> None: + state = self.state + repo = state.repository(owner, name) + pr = state.pull_request(repo, number) + if "title" in input and input["title"] is not None: + pr.title = input["title"] + if "base" in input and input["base"] is not None: + pr.baseRefName = input["base"] + pr.baseRef = await repo._make_ref_async(state, pr.baseRefName) + if "body" in input and input["body"] is not None: + pr.body = input["body"] + def _create_issue_comment( self, owner: str, name: str, comment_id: int, input: CreateIssueCommentInput ) -> CreateIssueCommentPayload: @@ -457,7 +523,22 @@ def _set_default_branch( repo = state.repository(owner, name) repo.defaultBranchRef = repo._make_ref(state, input["default_branch"]) + async def _set_default_branch_async( + self, owner: str, name: str, input: SetDefaultBranchInput + ) -> None: + state = self.state + repo = state.repository(owner, name) + repo.defaultBranchRef = await repo._make_ref_async( + state, input["default_branch"] + ) + def rest(self, method: str, path: str, **kwargs: Any) -> Any: + return self._rest_impl(method, path, **kwargs) + + async def arest(self, method: str, path: str, **kwargs: Any) -> Any: + return await self._arest_impl(method, path, **kwargs) + + def _rest_impl(self, method: str, path: str, **kwargs: Any) -> Any: if method == "get": m = re.match(r"^repos/([^/]+)/([^/]+)/branches/([^/]+)/protection", path) if m: @@ -472,6 +553,12 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any: "state": "closed" if pr.closed else "open", "title": pr.title, "body": pr.body, + "head": { + "ref": pr.headRefName, + }, + "base": { + "ref": pr.baseRefName, + }, } if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/comments/([^/]+)$", path): state = self.state @@ -536,3 +623,52 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any: raise NotImplementedError( "FakeGitHubEndpoint REST {} {} not implemented".format(method.upper(), path) ) + + async def _arest_impl(self, method: str, path: str, **kwargs: Any) -> Any: + if method == "get": + return self._rest_impl(method, path, **kwargs) + + elif method == "post": + if m := re.match(r"^repos/([^/]+)/([^/]+)/pulls$", path): + return await self._create_pull_async( + m.group(1), m.group(2), cast(CreatePullRequestInput, kwargs) + ) + if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/([^/]+)/comments", path): + return self._create_issue_comment( + m.group(1), + m.group(2), + GitHubNumber(int(m.group(3))), + cast(CreateIssueCommentInput, kwargs), + ) + if m := re.match( + r"^repos/([^/]+)/([^/]+)/pulls/([^/]+)/requested_reviewers", path + ): + return self._rest_impl(method, path, **kwargs) + if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/([^/]+)/labels", path): + return self._rest_impl(method, path, **kwargs) + + elif method == "patch": + if m := re.match(r"^repos/([^/]+)/([^/]+)(?:/pulls/([^/]+))?$", path): + owner, name, number = m.groups() + if number is not None: + return await self._update_pull_async( + owner, + name, + GitHubNumber(int(number)), + cast(UpdatePullRequestInput, kwargs), + ) + elif "default_branch" in kwargs: + return await self._set_default_branch_async( + owner, name, cast(SetDefaultBranchInput, kwargs) + ) + if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/comments/([^/]+)$", path): + return self._update_issue_comment( + m.group(1), + m.group(2), + int(m.group(3)), + cast(UpdateIssueCommentInput, kwargs), + ) + + raise NotImplementedError( + "FakeGitHubEndpoint REST {} {} not implemented".format(method.upper(), path) + ) diff --git a/src/ghstack/github_real.py b/src/ghstack/github_real.py index 600fe64..d949df4 100644 --- a/src/ghstack/github_real.py +++ b/src/ghstack/github_real.py @@ -1,11 +1,14 @@ #!/usr/bin/env python3 +import asyncio import json import logging import re +import ssl import time from typing import Any, Dict, Optional, Sequence, Tuple, Union +import aiohttp import requests import ghstack.github @@ -120,12 +123,6 @@ def graphql(self, query: str, **kwargs: Any) -> Any: return r - def _proxies(self) -> Dict[str, str]: - if self.proxy: - return {"http": self.proxy, "https": self.proxy} - else: - return {} - def get_head_ref(self, **params: Any) -> str: if self.oauth_token: @@ -149,15 +146,40 @@ def get_head_ref(self, **params: Any) -> str: # couldn't find, fall back to regular query return super().get_head_ref(**params) - def rest(self, method: str, path: str, **kwargs: Any) -> Any: + def _proxies(self) -> Dict[str, str]: + if self.proxy: + return {"http": self.proxy, "https": self.proxy} + else: + return {} + + def _rest_headers(self) -> Dict[str, str]: assert self.oauth_token - headers = { + return { "Authorization": "token " + self.oauth_token, "Content-Type": "application/json", "User-Agent": "ghstack", "Accept": "application/vnd.github.v3+json", } + def _aiohttp_ssl(self) -> Any: + if self.verify is False: + return False + if self.verify is None and self.cert is None: + return None + + context = ssl.create_default_context( + cafile=self.verify if isinstance(self.verify, str) else None + ) + if isinstance(self.cert, tuple): + context.load_cert_chain(self.cert[0], self.cert[1]) + elif self.cert is not None: + context.load_cert_chain(self.cert) + return context + + def rest(self, method: str, path: str, **kwargs: Any) -> Any: + assert self.oauth_token + headers = self._rest_headers() + url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path backoff_seconds = INITIAL_BACKOFF_SECONDS @@ -179,7 +201,7 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any: try: r = resp.json() except ValueError: - logging.debug("Response body:\n{}".format(r.text)) + logging.debug("Response body:\n{}".format(resp.text)) raise else: pretty_json = json.dumps(r, indent=1) @@ -241,3 +263,98 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any: return r raise RuntimeError("Exceeded maximum retries due to GitHub rate limiting") + + async def arest(self, method: str, path: str, **kwargs: Any) -> Any: + assert self.oauth_token + headers = self._rest_headers() + url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path + + request_kwargs: Dict[str, Any] = { + "json": kwargs, + "headers": headers, + } + if self.proxy: + request_kwargs["proxy"] = self.proxy + aiohttp_ssl = self._aiohttp_ssl() + if aiohttp_ssl is not None: + request_kwargs["ssl"] = aiohttp_ssl + + backoff_seconds = INITIAL_BACKOFF_SECONDS + async with aiohttp.ClientSession() as session: + for attempt in range(0, MAX_RETRIES): + logging.debug("# {} {}".format(method, url)) + logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1))) + + async with getattr(session, method)(url, **request_kwargs) as resp: + logging.debug("Response status: {}".format(resp.status)) + try: + r = await resp.json() + except (aiohttp.ContentTypeError, ValueError): + logging.debug("Response body:\n{}".format(await resp.text())) + raise + else: + pretty_json = json.dumps(r, indent=1) + logging.debug("Response JSON:\n{}".format(pretty_json)) + + if resp.status in (403, 429): + remaining_count = resp.headers.get("x-ratelimit-remaining") + reset_time = resp.headers.get("x-ratelimit-reset") + + if remaining_count == "0" and reset_time: + sleep_time = int(reset_time) - int(time.time()) + logging.warning( + f"Rate limit exceeded. Sleeping until reset in {sleep_time} seconds." + ) + await asyncio.sleep(sleep_time) + continue + else: + retry_after_seconds = resp.headers.get("retry-after") + if retry_after_seconds: + sleep_time = int(retry_after_seconds) + logging.warning( + f"Secondary rate limit hit. Sleeping for {sleep_time} seconds." + ) + else: + sleep_time = backoff_seconds + logging.warning( + f"Secondary rate limit hit. Sleeping for {sleep_time} seconds (exponential backoff)." + ) + backoff_seconds *= 2 + await asyncio.sleep(sleep_time) + continue + + if resp.status == 404: + raise ghstack.github.NotFoundError( + """\ +GitHub raised a 404 error on the request for +{url}. +Usually, this doesn't actually mean the page doesn't exist; instead, it +usually means that you didn't configure your OAuth token with enough +permissions. Please create a new OAuth token at +https://{github_url}/settings/tokens and DOUBLE CHECK that you checked +"public_repo" for permissions, and update ~/.ghstackrc with your new +value. + +Another possible reason for this error is if the repository has moved +to a new location or been renamed. Check that the repository URL is +still correct. +""".format( + url=url, github_url=self.github_url + ) + ) + + if resp.status >= 400: + raise RuntimeError(pretty_json) + + return r + + raise RuntimeError("Exceeded maximum retries due to GitHub rate limiting") + + async def aget(self, path: str, **kwargs: Any) -> Any: + return await self.arest("get", path, **kwargs) + + async def apost(self, path: str, **kwargs: Any) -> Any: + return await self.arest("post", path, **kwargs) + + async def apatch(self, path: str, **kwargs: Any) -> Any: + return await self.arest("patch", path, **kwargs) diff --git a/src/ghstack/shell.py b/src/ghstack/shell.py index 91d1f6a..681babd 100644 --- a/src/ghstack/shell.py +++ b/src/ghstack/shell.py @@ -197,8 +197,11 @@ async def run() -> Tuple[int, bytes, bytes]: assert proc.returncode is not None return (proc.returncode, out, err) - loop = asyncio.get_event_loop() - returncode, out, err = loop.run_until_complete(run()) + loop = asyncio.new_event_loop() + try: + returncode, out, err = loop.run_until_complete(run()) + finally: + loop.close() def decode(b: bytes) -> str: return ( @@ -327,7 +330,15 @@ def open(self, fn: str, mode: str) -> IO[Any]: fn: filename to open mode: mode to open the file as """ - return open(os.path.join(self.cwd, fn), mode) + return open(self.abspath(fn), mode) + + def abspath(self, fn: str) -> str: + """ + Resolve a path against this shell's current working directory. + """ + if os.path.isabs(fn): + return fn + return os.path.join(self.cwd, fn) def cd(self, d: str) -> None: """ From cf52e4c3999400c5c9ff3aa02a74c988b213cad4 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 11:58:52 -0400 Subject: [PATCH 02/18] Update [ghstack-poisoned] --- README.md | 5 ++++ src/ghstack/github_utils.py | 35 ++++++++++++++++++++++--- src/ghstack/test_prelude.py | 1 + test/land/default_branch_change.py.test | 10 +++++++ 4 files changed, 47 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c00898b..5ee4b49 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,11 @@ do that. (There's also a more fundamental reason why this won't work: since each commit is a separate PR, you have to resolve conflicts in *each* PR, not just for the entire stack.) +**What if the repository default branch changed?** ghstack caches +repository metadata in `.git/ghstack-repo-info.json` for the local +checkout. If ghstack is still using an old default branch name, +delete that file and rerun ghstack; it will query GitHub again. + **How do I start a new feature?** Just checkout main on a new branch, and start working on a fresh branch. diff --git a/src/ghstack/github_utils.py b/src/ghstack/github_utils.py index 1bbf027..6a5b9ca 100644 --- a/src/ghstack/github_utils.py +++ b/src/ghstack/github_utils.py @@ -1,5 +1,8 @@ #!/usr/bin/env python3 +import json +import logging +import os import re from typing import Optional @@ -60,6 +63,11 @@ def get_github_repo_name_with_owner( ) +def _repo_info_cache_path(sh: ghstack.shell.Shell) -> str: + git_dir = sh.abspath(sh.git("rev-parse", "--git-dir")) + return os.path.join(git_dir, "ghstack-repo-info.json") + + def get_github_repo_info( *, github: ghstack.github.GitHubEndpoint, @@ -78,7 +86,20 @@ def get_github_repo_info( else: name_with_owner = {"owner": repo_owner, "name": repo_name} - # TODO: Cache this guy + cache_path = _repo_info_cache_path(sh) + try: + with open(cache_path) as f: + cached = json.load(f) + if ( + cached.get("name_with_owner") == name_with_owner + and cached.get("id") + and cached.get("default_branch") + ): + logging.debug("Using cached repo info from %s", cache_path) + return cached + except (OSError, json.JSONDecodeError, KeyError): + pass + try: repo = github.graphql( """ @@ -95,7 +116,6 @@ def get_github_repo_info( name=name_with_owner["name"], )["data"]["repository"] except RuntimeError as e: - # Check if this is a repository access error (NOT_FOUND) error_msg = str(e) if ( "Could not resolve to a Repository" in error_msg @@ -114,16 +134,23 @@ def get_github_repo_info( f"3. Make sure to grant access to the appropriate organizations when prompted" ) else: - # Re-raise the original error if it's not the repository access issue raise - return { + result: GitHubRepoInfo = { "name_with_owner": name_with_owner, "id": repo["id"], "is_fork": repo["isFork"], "default_branch": repo["defaultBranchRef"]["name"], } + try: + with open(cache_path, "w") as f: + json.dump(result, f) + except OSError: + pass + + return result + RE_PR_URL = re.compile( r"^https://(?P[^/]+)/(?P[^/]+)/(?P[^/]+)/pull/(?P[0-9]+)/?$" diff --git a/src/ghstack/test_prelude.py b/src/ghstack/test_prelude.py index 11274e5..0607167 100644 --- a/src/ghstack/test_prelude.py +++ b/src/ghstack/test_prelude.py @@ -119,6 +119,7 @@ def __init__(self, direct: bool) -> None: local_dir = tempfile.mkdtemp() self.sh = ghstack.shell.Shell(cwd=local_dir, testing=True) self.sh.git("clone", upstream_dir, ".") + self.sh.git("fetch", "origin", "+refs/heads/*:refs/remotes/origin/*") self.direct = direct def cleanup(self) -> None: diff --git a/test/land/default_branch_change.py.test b/test/land/default_branch_change.py.test index 7160e23..b44cd01 100644 --- a/test/land/default_branch_change.py.test +++ b/test/land/default_branch_change.py.test @@ -1,3 +1,4 @@ +import os from ghstack.test_prelude import * init_test() @@ -15,6 +16,13 @@ get_github().patch( name="pytorch", default_branch="main", ) +# invalidate repo info cache since default branch changed +cache_path = os.path.join( + get_sh().abspath(get_sh().git("rev-parse", "--git-dir")), + "ghstack-repo-info.json", +) +if os.path.exists(cache_path): + os.remove(cache_path) assert_github_state( """\ @@ -63,6 +71,8 @@ get_github().patch( name="pytorch", default_branch="master", ) +if os.path.exists(cache_path): + os.remove(cache_path) assert_github_state( """\ From bbbd7a9ca9d3f681e428488cd382ad65e7afd774 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 11:58:52 -0400 Subject: [PATCH 03/18] Update [ghstack-poisoned] --- src/ghstack/cli.py | 7 + src/ghstack/submit.py | 644 ++++++++++++++++++++++++++++-------------- 2 files changed, 438 insertions(+), 213 deletions(-) diff --git a/src/ghstack/cli.py b/src/ghstack/cli.py index 25f8e3a..1d95ec2 100644 --- a/src/ghstack/cli.py +++ b/src/ghstack/cli.py @@ -384,6 +384,11 @@ def status(pull_request: str) -> None: is_flag=True, help="Create stack that directly merges into main", ) +@click.option( + "--no-fetch", + is_flag=True, + help="Skip fetching remote refs (faster when you know local refs are up-to-date)", +) @click.argument( "revs", nargs=-1, @@ -402,6 +407,7 @@ def submit( stack: bool, reviewer: Optional[str], label: Optional[str], + no_fetch: bool, ) -> None: """ Submit or update a PR stack @@ -425,6 +431,7 @@ def submit( direct_opt=direct_opt, reviewer=reviewer if reviewer is not None else config.reviewer, label=label if label is not None else config.label, + no_fetch=no_fetch, ) diff --git a/src/ghstack/submit.py b/src/ghstack/submit.py index 808dc11..6a94126 100644 --- a/src/ghstack/submit.py +++ b/src/ghstack/submit.py @@ -1,12 +1,26 @@ #!/usr/bin/env python3 +import asyncio import dataclasses import itertools import logging import os import re +import time from dataclasses import dataclass -from typing import Any, Dict, Iterator, List, Optional, Sequence, Set, Tuple +from typing import ( + Any, + Awaitable, + Dict, + Iterable, + Iterator, + List, + Optional, + Sequence, + Set, + Tuple, + TypeVar, +) import ghstack import ghstack.git @@ -256,6 +270,60 @@ def next(self) -> GitCommitHash: return self.push_branches.next.commit.commit_id +_TIMING_ENABLED = True +_T = TypeVar("_T") + + +class _Timer: + def __init__(self) -> None: + self.start = time.monotonic() + self.last = self.start + self.entries: List[Tuple[str, float]] = [] + + def mark(self, label: str) -> None: + now = time.monotonic() + self.entries.append((label, now - self.last)) + self.last = now + + def report(self) -> None: + total = time.monotonic() - self.start + for label, elapsed in self.entries: + logging.info("[ghstack timing] %s: %.0fms", label, elapsed * 1000) + logging.info("[ghstack timing] total: %.0fms", total * 1000) + + +def _run_async_ordered(awaitables: Iterable[Awaitable[_T]]) -> List[_T]: + aws = list(awaitables) + if not aws: + return [] + + async def gather() -> List[Any]: + return await asyncio.gather(*aws, return_exceptions=True) + + loop = asyncio.new_event_loop() + try: + results = loop.run_until_complete(gather()) + finally: + loop.close() + + checked_results: List[_T] = [] + for result in results: + if isinstance(result, BaseException): + raise result + checked_results.append(result) + return checked_results + + +@dataclass +class _PendingNewPR: + commit_id: GitCommitHash + diff: ghstack.diff.Diff + base_diff_meta: Optional["DiffMeta"] + ghnum: GhNumber + push_specs: List[str] + diff_meta: "DiffMeta" + + def main(**kwargs: Any) -> List[DiffMeta]: submitter = Submitter(**kwargs) return submitter.run() @@ -269,8 +337,9 @@ def all_branches(username: str, ghnum: GhNumber) -> Tuple[str, str, str]: ) -def push_spec(commit: GitCommitHash, branch: str) -> str: - return "{}:refs/heads/{}".format(commit, branch) +def push_spec(commit: GitCommitHash, branch: str, force: bool = False) -> str: + spec = "{}:refs/heads/{}".format(commit, branch) + return "+" + spec if force else spec @dataclass(frozen=True) @@ -350,6 +419,9 @@ class Submitter: # Default labels to add to new pull requests (comma-separated labels) label: Optional[str] = None + # Skip fetching remote refs before submitting + no_fetch: bool = False + # ~~~~~~~~~~~~~~~~~~~~~~~~ # Computed in post init @@ -429,7 +501,21 @@ def __post_init__(self) -> None: # The main algorithm def run(self) -> List[DiffMeta]: - self.fetch() + timer = _Timer() if _TIMING_ENABLED else None + + if not self.no_fetch: + # Submit only needs fresh ghstack refs here. We intentionally do + # not fetch the base branch: in the normal workflow, a stack based + # on newer upstream commits got those commits by updating the local + # base ref before rebasing. The later rev-list boundary is against + # that local base ref, so narrowing this fetch avoids unrelated + # remote IO while preserving the usual submit semantics. + self.fetch( + f"+refs/heads/gh/{self.username}/*" + f":refs/remotes/{self.remote_name}/gh/{self.username}/*" + ) + if timer: + timer.mark("fetch") commits_to_submit_and_boundary = self.parse_revs() @@ -474,12 +560,19 @@ def run(self) -> List[DiffMeta]: "There appears to be no commits to process, based on the revs you passed me." ) + if timer: + timer.mark("parse_revs") + # This is not really accurate if you're doing a fancy pattern; # if this is a problem file us a bug. run_pre_ghstack_hook( self.sh, f"{self.remote_name}/{self.base}", commits_to_submit[0].commit_id ) + pr_info_cache = self._prefetch_pr_info(commits_to_rebase) + if not self.no_fetch: + self._fetch_foreign_pr_refs(pr_info_cache.values()) + # NB: This is duplicative with prepare_submit to keep the # check_invariants code small, as it counts as TCB pre_branch_state_index: Dict[GitCommitHash, PreBranchState] = {} @@ -487,7 +580,10 @@ def run(self) -> List[DiffMeta]: for h in commits_to_submit: d = ghstack.git.convert_header(h, self.github_url) if d.pull_request_resolved is not None: - ed = self.elaborate_diff(d) + ed = self.elaborate_diff( + d, + _pr_info=pr_info_cache.get(d.pull_request_resolved.number), + ) # Skip closed PRs (e.g., after landing) where branches have been deleted if not ed.closed: pre_branch_state_index[h.commit_id] = PreBranchState( @@ -511,8 +607,13 @@ def run(self) -> List[DiffMeta]: ) } diff_meta_index, rebase_index = self.prepare_updates( - commit_index, commits_to_submit, commits_to_rebase + commit_index, + commits_to_submit, + commits_to_rebase, + pr_info_cache=pr_info_cache, ) + if timer: + timer.mark("prepare_updates") logging.debug("rebase_index = %s", rebase_index) diffs_to_submit = [ diff_meta_index[h.commit_id] @@ -525,6 +626,8 @@ def run(self) -> List[DiffMeta]: if h.commit_id in diff_meta_index ] self.push_updates(diffs_to_submit, all_diffs=all_diffs_in_topo_order) + if timer: + timer.mark("push_updates") if new_head := rebase_index.get( old_head := GitCommitHash(self.sh.git("rev-parse", "HEAD")) ): @@ -564,22 +667,31 @@ def run(self) -> List[DiffMeta]: exitcode=True, ) + if timer: + timer.mark("finalize") + timer.report() + # NB: earliest first, which is the intuitive order for unit testing return list(reversed(diffs_to_submit)) # ~~~~~~~~~~~~~~~~~~~~~~~~ # The main pieces - def fetch(self) -> None: - # TODO: Potentially we could narrow this refspec down to only OUR gh - # branches. However, this will interact poorly with cross-author - # so it needs to be thought more carefully - self.sh.git( - "fetch", - "--prune", - self.remote_name, - f"+refs/heads/*:refs/remotes/{self.remote_name}/*", - ) + def fetch(self, refspec: Optional[str] = None) -> None: + if refspec is not None: + self.sh.git( + "fetch", + "--prune", + self.remote_name, + refspec, + ) + else: + self.sh.git( + "fetch", + "--prune", + self.remote_name, + f"+refs/heads/*:refs/remotes/{self.remote_name}/*", + ) def parse_revs(self) -> List[ghstack.git.CommitHeader]: # There are two distinct usage patterns: @@ -679,20 +791,71 @@ def parse_revs(self) -> List[ghstack.git.CommitHeader]: return commits_to_submit_and_boundary + def _prefetch_pr_info( + self, + commits: List[ghstack.git.CommitHeader], + ) -> Dict[GitHubNumber, Any]: + """Batch-fetch PR info for all commits that have existing PRs. + Uses async REST calls to overlap GitHub IO.""" + + pr_numbers: List[GitHubNumber] = [] + for commit in commits: + diff = ghstack.git.convert_header(commit, self.github_url) + if diff.pull_request_resolved is not None: + pr_numbers.append(diff.pull_request_resolved.number) + + if not pr_numbers: + return {} + + unique_numbers = sorted(set(pr_numbers), key=int) + + async def fetch_pr(number: GitHubNumber) -> Tuple[GitHubNumber, Any]: + r = await self.github.arest( + "get", f"repos/{self.repo_owner}/{self.repo_name}/pulls/{number}" + ) + return number, r + + results = _run_async_ordered(fetch_pr(number) for number in unique_numbers) + pr_info: Dict[GitHubNumber, Any] = dict(results) + + return pr_info + + def _fetch_foreign_pr_refs(self, pr_infos: Iterable[Any]) -> None: + usernames: Set[str] = set() + for pr_info in pr_infos: + head_ref_name = self._pr_ref_name(pr_info, "head") + if head_ref_name is None: + continue + m = re.match(r"gh/([^/]+)/([0-9]+)/head$", head_ref_name) + if m is not None and m.group(1) != self.username: + usernames.add(m.group(1)) + + for username in sorted(usernames): + self.fetch( + f"+refs/heads/gh/{username}/*" + f":refs/remotes/{self.remote_name}/gh/{username}/*" + ) + def prepare_updates( self, commit_index: Dict[GitCommitHash, ghstack.git.CommitHeader], commits_to_submit: List[ghstack.git.CommitHeader], commits_to_rebase: List[ghstack.git.CommitHeader], + *, + pr_info_cache: Optional[Dict[GitHubNumber, Any]] = None, ) -> Tuple[Dict[GitCommitHash, DiffMeta], Dict[GitCommitHash, GitCommitHash]]: - # Prepare diffs in reverse topological order. - # (Reverse here is important because we must have processed parents - # first.) - # NB: some parts of the algo (namely commit creation) could - # be done in parallel + # Prefetch PR info for all commits with existing PRs (parallel REST GETs) + if pr_info_cache is None: + pr_info_cache = self._prefetch_pr_info(commits_to_rebase) + + # Phase 1: Process all commits (oldest first) to determine what + # needs updating, create head/base commits, and identify new PRs. + # New PRs are NOT pushed/created yet — deferred to batch operation. submit_set = set(h.commit_id for h in commits_to_submit) diff_meta_index: Dict[GitCommitHash, DiffMeta] = {} - rebase_index: Dict[GitCommitHash, GitCommitHash] = {} + pending_new_prs: List[_PendingNewPR] = [] + object.__setattr__(self, "_pending_new_prs", pending_new_prs) + for commit in reversed(commits_to_rebase): submit = commit.commit_id in submit_set parents = commit.parents @@ -704,7 +867,6 @@ def prepare_updates( ) ) parent = parents[0] - diff_meta = None parent_commit = commit_index[parent] parent_diff_meta = diff_meta_index.get(parent) diff = ghstack.git.convert_header(commit, self.github_url) @@ -713,7 +875,10 @@ def prepare_updates( parent_diff_meta, diff, ( - self.elaborate_diff(diff) + self.elaborate_diff( + diff, + _pr_info=pr_info_cache.get(diff.pull_request_resolved.number), + ) if diff.pull_request_resolved is not None else None ), @@ -722,27 +887,64 @@ def prepare_updates( if diff_meta is not None: diff_meta_index[commit.commit_id] = diff_meta - # Check if we actually need to rebase it, or can use it as is - # NB: This is not in process_commit, because we may need - # to rebase a commit even if we didn't submit it - if parent in rebase_index or diff_meta is not None: - # Yes, we need to rebase it + # Phase 2: Batch-push branches and create all new PRs. + if self._pending_new_prs: + # Collect all push specs and push in one call. + all_new_push_specs: List[str] = [] + for pending in self._pending_new_prs: + all_new_push_specs.extend(pending.push_specs) + if all_new_push_specs: + self._git_push(all_new_push_specs) + + # Create PRs in stack order. GitHub PR numbers are globally allocated, + # so parallel creation makes numbering nondeterministic. + results = [ + ( + pending, + self._create_pull_request( + pending.diff, + pending.base_diff_meta, + pending.ghnum, + ), + ) + for pending in self._pending_new_prs + ] + # Update DiffMeta entries with real PR info + for pending, elab_diff in results: + dm = pending.diff_meta + dm.elab_diff = elab_diff + trailers_to_add = [f"ghstack-source-id: {pending.diff.source_id}"] + if self.direct: + trailers_to_add.append( + f"ghstack-comment-id: {elab_diff.comment_id}" + ) + trailers_to_add.append( + f"Pull-Request: {elab_diff.pull_request_resolved.url()}" + ) + dm.commit_msg = ghstack.trailers.interpret_trailers( + strip_mentions(pending.diff.summary.rstrip()), + trailers_to_add, + ) + + # Phase 3: Create orig commits and build rebase index. + # Must happen after Phase 2 so new PRs have correct commit messages. + rebase_index: Dict[GitCommitHash, GitCommitHash] = {} + for commit in reversed(commits_to_rebase): + parent = commit.parents[0] + diff_meta = diff_meta_index.get(commit.commit_id) + + if parent in rebase_index or diff_meta is not None: if diff_meta is not None: - # use the updated commit message, if it exists commit_msg = diff_meta.commit_msg else: commit_msg = commit.commit_msg - if rebase_id := rebase_index.get(commit.parents[0]): - # use the updated base, if it exists + if rebase_id := rebase_index.get(parent): base_commit_id = rebase_id else: base_commit_id = parent - # Preserve authorship of original commit - # (TODO: for some reason, we didn't do this for old commits, - # maybe it doesn't matter) env = {} if commit.author_name is not None: env["GIT_AUTHOR_NAME"] = commit.author_name @@ -762,9 +964,6 @@ def prepare_updates( ) if diff_meta is not None: - # Add the new_orig to push - # This may not exist. If so, that means this diff only exists - # to update HEAD. diff_meta.push_branches.orig.update(GhCommit(new_orig, commit.tree)) rebase_index[commit.commit_id] = new_orig @@ -772,11 +971,15 @@ def prepare_updates( return diff_meta_index, rebase_index def elaborate_diff( - self, diff: ghstack.diff.Diff, *, is_ghexport: bool = False + self, + diff: ghstack.diff.Diff, + *, + is_ghexport: bool = False, + _pr_info: Any = None, ) -> DiffWithGitHubMetadata: """ - Query GitHub API for the current title, body and closed? status - of the pull request corresponding to a ghstack.diff.Diff. + Query GitHub API for the current title, body, branch, and closed? + status of the pull request corresponding to a ghstack.diff.Diff. """ assert diff.pull_request_resolved is not None @@ -784,92 +987,72 @@ def elaborate_diff( assert diff.pull_request_resolved.repo == self.repo_name number = diff.pull_request_resolved.number - # TODO: There is no reason to do a node query here; we can - # just look up the repo the old fashioned way - r = self.github.graphql( - """ - query ($repo_id: ID!, $number: Int!) { - node(id: $repo_id) { - ... on Repository { - pullRequest(number: $number) { - body - title - closed - headRefName - baseRefName - } - } - } - } - """, - repo_id=self.repo_id, - number=number, - )["data"]["node"]["pullRequest"] - # Sorry, this is a big hack to support the ghexport case - m = re.match(r"(refs/heads/)?export-D([0-9]+)$", r["headRefName"]) - if m is not None and is_ghexport: + # Use pre-fetched PR info if available, otherwise fetch now. + if _pr_info is None: + pr_info = self.github.get( + f"repos/{self.repo_owner}/{self.repo_name}/pulls/{number}" + ) + else: + pr_info = _pr_info + + head_ref_name = self._pr_ref_name(pr_info, "head") + if head_ref_name is None: + head_ref_name = self.github.get_head_ref( + owner=self.repo_owner, name=self.repo_name, number=number + ) + + m_export = re.match(r"(refs/heads/)?export-D([0-9]+)$", head_ref_name) + if m_export is not None and is_ghexport: raise RuntimeError( - """\ -This commit appears to already be associated with a pull request, -but the pull request was previously submitted with an old version of -ghexport. You can continue exporting using the old style using: - - ghexport --legacy - -For future diffs, we recommend using the non-legacy version of ghexport -as it supports bidirectional syncing. However, there is no way to -convert a pre-existing PR in the old style to the new format which -supports bidirectional syncing. If you would like to blow away the old -PR and start anew, edit the Summary in the Phabricator diff to delete -the line 'Pull-Request' and then run ghexport again. -""" + "This commit appears to already be associated with a pull request,\n" + "but the pull request was previously submitted with an old version of\n" + "ghexport. You can continue exporting using the old style using:\n\n" + " ghexport --legacy\n\n" + "For future diffs, we recommend using the non-legacy version of ghexport\n" + "as it supports bidirectional syncing. However, there is no way to\n" + "convert a pre-existing PR in the old style to the new format which\n" + "supports bidirectional syncing. If you would like to blow away the old\n" + "PR and start anew, edit the Summary in the Phabricator diff to delete\n" + "the line 'Pull-Request' and then run ghexport again.\n" ) - # TODO: Hmm, I'm not sure why this matches - m = re.match(r"gh/([^/]+)/([0-9]+)/head$", r["headRefName"]) + m = re.match(r"gh/([^/]+)/([0-9]+)/head$", head_ref_name) if m is None: if is_ghexport: raise RuntimeError( - """\ -This commit appears to already be associated with a pull request, -but the pull request doesn't look like it was submitted by ghexport -Maybe you exported it using the "Export to Open Source" button on -the Phabricator diff page? If so, please continue to use that button -to export your diff. - -If you think this is in error, edit the Summary in the Phabricator diff -to delete the line 'Pull-Request' and then run ghexport again. -""" - ) - else: - raise RuntimeError( - """\ -This commit appears to already be associated with a pull request, -but the pull request doesn't look like it was submitted by ghstack. -If you think this is in error, run: - - ghstack unlink {} - -to disassociate the commit with the pull request, and then try again. -(This will create a new pull request!) -""".format( - diff.oid - ) + "This commit appears to already be associated with a pull request,\n" + "but the pull request doesn't look like it was submitted by ghexport\n" + 'Maybe you exported it using the "Export to Open Source" button on\n' + "the Phabricator diff page? If so, please continue to use that button\n" + "to export your diff.\n\n" + "If you think this is in error, edit the Summary in the Phabricator diff\n" + "to delete the line 'Pull-Request' and then run ghexport again.\n" ) + raise RuntimeError( + "This commit appears to already be associated with a pull request,\n" + "but the pull request doesn't look like it was submitted by ghstack.\n" + "If you think this is in error, run:\n\n" + " ghstack unlink {}\n\n" + "to disassociate the commit with the pull request, and then try again.\n" + "(This will create a new pull request!)\n".format(diff.oid) + ) username = m.group(1) gh_number = GhNumber(m.group(2)) - # NB: Technically, we don't need to pull this information at - # all, but it's more convenient to unconditionally edit - # title/body when we update the pull request info - title = r["title"] - pr_body = r["body"] - if self.update_fields: - title, pr_body = self._default_title_and_body(diff, pr_body) + base_ref_name = self._pr_ref_name(pr_info, "base") + if base_ref_name is None: + if self.direct: + base_ref_name = str(branch_next(username, gh_number)) + else: + base_ref_name = str(branch_base(username, gh_number)) + closed = pr_info.get("state") != "open" - # TODO: remote summary should be done earlier so we can use - # it to test if updates are necessary + if self.update_fields: + title, pr_body = self._default_title_and_body(diff, pr_info.get("body")) + else: + title = pr_info["title"] + pr_body = pr_info["body"] or "" try: rev_list = self.sh.git( @@ -879,11 +1062,7 @@ def elaborate_diff( self.remote_name + "/" + branch_orig(username, gh_number), ) except RuntimeError: - if r["closed"]: - # If the PR is closed and the branch is deleted (e.g., after landing), - # we can't get the remote source ID. Return None for it, which will - # signal to process_commit that this commit has been landed and should - # be skipped (not updated). + if closed: remote_source_id = None comment_id = None else: @@ -901,17 +1080,28 @@ def elaborate_diff( diff=diff, title=title, body=pr_body, - closed=r["closed"], + closed=closed, number=number, username=username, ghnum=gh_number, remote_source_id=remote_source_id, comment_id=comment_id, pull_request_resolved=diff.pull_request_resolved, - head_ref=r["headRefName"], - base_ref=r["baseRefName"], + head_ref=head_ref_name, + base_ref=base_ref_name, ) + def _pr_ref_name(self, pr_info: Any, kind: str) -> Optional[str]: + ref = pr_info.get(kind) + if isinstance(ref, dict): + name = ref.get("ref") + if isinstance(name, str): + return name + name = pr_info.get(f"{kind}RefName") + if isinstance(name, str): + return name + return None + def process_commit( self, base: ghstack.git.CommitHeader, @@ -990,14 +1180,54 @@ def process_commit( # Create pull request, if needed if elab_diff is None: - # Need to push branches now rather than later, so we can create PR - self._git_push( - [push_spec(p[0], branch(username, ghnum, p[1])) for p in push_branches] - ) + # Defer push and PR creation to batch phase. + # Record push specs for later, use placeholder commit_msg. + new_pr_push_specs = [ + push_spec(p[0], branch(username, ghnum, p[1])) for p in push_branches + ] push_branches.clear() - elab_diff = self._create_pull_request(diff, base_diff_meta, ghnum) - what = "Created" - new_pr = True + + # Placeholder elab_diff — will be replaced after PR creation + placeholder_elab = DiffWithGitHubMetadata( + diff=diff, + number=GitHubNumber(0), + username=username, + remote_source_id=diff.source_id, + comment_id=None, + title=diff.title, + body="", + closed=False, + ghnum=ghnum, + pull_request_resolved=ghstack.diff.PullRequestResolved( + owner=self.repo_owner, + repo=self.repo_name, + number=0, + github_url=self.github_url, + ), + head_ref=str(branch_head(username, ghnum)), + base_ref=base_branch, + ) + # Placeholder commit_msg — will be updated after PR creation + commit_msg = strip_mentions(diff.summary.rstrip()) + + dm = DiffMeta( + elab_diff=placeholder_elab, + commit_msg=commit_msg, + push_branches=push_branches, + what="Created", + base=base_branch, + ) + self._pending_new_prs.append( + _PendingNewPR( + commit_id=diff.oid, + diff=diff, + base_diff_meta=base_diff_meta, + ghnum=ghnum, + push_specs=new_pr_push_specs, + diff_meta=dm, + ) + ) + return dm else: if not push_branches: what = "Skipped" @@ -1005,36 +1235,17 @@ def process_commit( what = "Skipped (next updated)" else: what = "Updated" - new_pr = False - pull_request_resolved = elab_diff.pull_request_resolved - - if not new_pr: - # Underlying diff can be assumed to have the correct metadata, we - # only need to update it commit_msg = self._update_source_id(diff.summary, elab_diff) - else: - # Need to insert metadata for the first time - # Using our Python implementation of interpret-trailers - trailers_to_add = [f"ghstack-source-id: {diff.source_id}"] - if self.direct: - trailers_to_add.append(f"ghstack-comment-id: {elab_diff.comment_id}") - - trailers_to_add.append(f"Pull-Request: {pull_request_resolved.url()}") - - commit_msg = ghstack.trailers.interpret_trailers( - strip_mentions(diff.summary.rstrip()), trailers_to_add + return DiffMeta( + elab_diff=elab_diff, + commit_msg=commit_msg, + push_branches=push_branches, + what=what, + base=base_branch, ) - return DiffMeta( - elab_diff=elab_diff, - commit_msg=commit_msg, - push_branches=push_branches, - what=what, - base=base_branch, - ) - def _raise_poisoned(self) -> None: raise RuntimeError( """\ @@ -1068,30 +1279,28 @@ def _warn_empty( ) def _allocate_ghnum(self) -> GhNumber: - # Determine the next available GhNumber. We do this by - # iterating through known branches and keeping track - # of the max. The next available GhNumber is the next number. - # This is technically subject to a race, but we assume - # end user is not running this script concurrently on - # multiple machines (you bad bad) + # Check both seen_ghnums (from commits in the current stack) and + # remote refs (which may include ghnums from landed/closed PRs + # whose branches still exist) + max_seen = max( + ( + int(str(ghnum)) + for user, ghnum in self.seen_ghnums + if user == self.username + ), + default=0, + ) refs = self.sh.git( "for-each-ref", - # Use OUR username here, since there's none attached to the - # diff "refs/remotes/{}/gh/{}".format(self.remote_name, self.username), "--format=%(refname)", ).split() - - def _is_valid_ref(ref: str) -> bool: - splits = ref.split("/") - if len(splits) < 3: - return False - else: - return splits[-2].isnumeric() - - refs = list(filter(_is_valid_ref, refs)) - max_ref_num = max(int(ref.split("/")[-2]) for ref in refs) if refs else 0 - return GhNumber(str(max_ref_num + 1)) + max_ref = 0 + for ref in refs: + parts = ref.split("/") + if len(parts) >= 3 and parts[-2].isnumeric(): + max_ref = max(max_ref, int(parts[-2])) + return GhNumber(str(max(max_seen, max_ref) + 1)) def _sanity_check_ghnum(self, username: str, ghnum: GhNumber) -> None: if (username, ghnum) in self.seen_ghnums: @@ -1550,36 +1759,32 @@ def push_updates( all_diffs: Optional[List[DiffMeta]] = None, import_help: bool = True, ) -> None: - # update pull request information, update bases as necessary - # preferably do this in one network call - # push your commits (be sure to do this AFTER you update bases) - base_push_branches: List[str] = [] - push_branches: List[str] = [] - force_push_branches: List[str] = [] + # Collect all refspecs into a single batched push. This is being + # tested in production because GitHub may observe base/head ref updates + # out of order when refreshing PR diffs. If that happens, revert this + # block to three grouped pushes in this order: + # 1. base branches + # 2. head/next branches + # 3. orig branches + # Per-refspec force is encoded with the + prefix: + # orig branches: always force-pushed + # head/next branches: force-pushed only with --force flag + # base branches: never force-pushed + all_push_specs: List[str] = [] for s in reversed(diffs_to_submit): - # It is VERY important that we do base updates BEFORE real - # head updates, otherwise GitHub will spuriously think that - # the user pushed a number of patches as part of the PR, - # when actually they were just from the (new) upstream - # branch - for diff, b in s.push_branches: if b == "orig": - q = force_push_branches + force = True elif b == "base": - q = base_push_branches + force = False else: - q = push_branches - q.append(push_spec(diff, branch(s.username, s.ghnum, b))) - # Careful! Don't push main. - # TODO: These pushes need to be atomic (somehow) - if base_push_branches: - self._git_push(base_push_branches) - if push_branches: - self._git_push(push_branches, force=self.force) - if force_push_branches: - self._git_push(force_push_branches, force=True) + force = self.force + all_push_specs.append( + push_spec(diff, branch(s.username, s.ghnum, b), force=force) + ) + if all_push_specs: + self._git_push(all_push_specs) # Discover orphan PR numbers from the old stack listing. # We search the full local stack for old stack text, then @@ -1621,8 +1826,7 @@ def push_updates( orphan_above.append(num) break - for s in reversed(diffs_to_submit): - # NB: GraphQL API does not support modifying PRs + def _update_pr_args(s: DiffMeta) -> Tuple[str, Dict[str, Any], Optional[str]]: assert not s.closed logging.info( "# Updating https://{github_url}/{owner}/{repo}/pull/{number}".format( @@ -1632,7 +1836,6 @@ def push_updates( number=s.number, ) ) - # TODO: don't update this if it doesn't need updating base_kwargs = {} if self.direct: base_kwargs["base"] = s.base @@ -1641,24 +1844,39 @@ def push_updates( stack_desc = self._format_stack( diffs_to_submit, s.number, orphan_above, orphan_below ) - self.github.patch( - "repos/{owner}/{repo}/pulls/{number}".format( - owner=self.repo_owner, repo=self.repo_name, number=s.number - ), - # NB: this substitution does nothing on direct PRs - body=RE_STACK.sub( + path = "repos/{owner}/{repo}/pulls/{number}".format( + owner=self.repo_owner, repo=self.repo_name, number=s.number + ) + kwargs = { + "body": RE_STACK.sub( stack_desc, s.body, ), - title=s.title, + "title": s.title, **base_kwargs, - ) - + } + comment_path = None if s.elab_diff.comment_id is not None: - self.github.patch( - f"repos/{self.repo_owner}/{self.repo_name}/issues/comments/{s.elab_diff.comment_id}", - body=stack_desc, + comment_path = ( + f"repos/{self.repo_owner}/{self.repo_name}/issues/comments/" + f"{s.elab_diff.comment_id}" ) + return path, kwargs, comment_path + + async def _update_pr_async(s: DiffMeta) -> None: + path, kwargs, comment_path = _update_pr_args(s) + await self.github.arest("patch", path, **kwargs) + + if comment_path is not None: + await self.github.arest( + "patch", + comment_path, + body=self._format_stack( + diffs_to_submit, s.number, orphan_above, orphan_below + ), + ) + + _run_async_ordered(_update_pr_async(s) for s in reversed(diffs_to_submit)) # Report what happened def format_url(s: DiffMeta) -> str: From 4303245817765cee3a626a16ae28144e3bfe6edb Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 11:58:52 -0400 Subject: [PATCH 04/18] Update [ghstack-poisoned] --- bench/bench_submit.py | 216 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 216 insertions(+) create mode 100644 bench/bench_submit.py diff --git a/bench/bench_submit.py b/bench/bench_submit.py new file mode 100644 index 0000000..4376517 --- /dev/null +++ b/bench/bench_submit.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +""" +Benchmark ghstack submit against a real GitHub repository. + +Usage: + python bench/bench_submit.py --repo owner/repo [--token TOKEN] [--iterations N] [--stack-size N] + +The repo should be a throwaway playground repo you don't mind having +test PRs created in. PRs are closed (not deleted) after each run. + +Requires GITHUB_TOKEN env var or --token flag. +""" + +import argparse +import os +import re +import shutil +import statistics +import subprocess +import sys +import tempfile +from typing import Dict, List, Tuple + + +def run(args: List[str], cwd: str, **kwargs) -> subprocess.CompletedProcess: + return subprocess.run(args, cwd=cwd, capture_output=True, text=True, **kwargs) + + +def git(cwd: str, *args: str, check: bool = True) -> str: + r = run(["git", *args], cwd=cwd) + if check and r.returncode != 0: + print(f"git {' '.join(args)} failed:\n{r.stderr}", file=sys.stderr) + raise RuntimeError(f"git failed: {r.stderr}") + return r.stdout.strip() + + +def parse_timing(stderr: str) -> Dict[str, float]: + timing: Dict[str, float] = {} + for line in stderr.splitlines(): + m = re.match(r"\[ghstack timing\] (.+): (\d+)ms", line) + if m: + timing[m.group(1)] = float(m.group(2)) + return timing + + +def close_prs(repo: str, token: str, pr_numbers: List[int]) -> None: + """Close PRs via GitHub API.""" + import requests + + headers = { + "Authorization": f"token {token}", + "Accept": "application/vnd.github.v3+json", + } + for num in pr_numbers: + requests.patch( + f"https://api.github.com/repos/{repo}/pulls/{num}", + headers=headers, + json={"state": "closed"}, + ) + + +def extract_pr_numbers(stdout: str) -> List[int]: + return [int(m) for m in re.findall(r"/pull/(\d+)", stdout)] + + +def run_benchmark( + repo: str, + token: str, + stack_size: int, + username: str, +) -> Tuple[Dict[str, float], Dict[str, float]]: + """Run one benchmark iteration. Returns (create_timing, update_timing).""" + workdir = tempfile.mkdtemp(prefix="ghstack-bench-") + try: + # Clone + git( + workdir, + "clone", + f"https://x-access-token:{token}@github.com/{repo}.git", + "repo", + ) + repo_dir = os.path.join(workdir, "repo") + git(repo_dir, "config", "user.name", "ghstack-bench") + git(repo_dir, "config", "user.email", "bench@ghstack.dev") + + # Create N commits + for i in range(stack_size): + fname = os.path.join(repo_dir, f"bench_{i}.txt") + with open(fname, "w") as f: + f.write(f"commit {i}\n") + git(repo_dir, "add", fname) + git( + repo_dir, + "commit", + "-m", + f"Bench commit {i}\n\nThis is bench commit {i}", + ) + + # First submit (creates PRs) + env = { + **os.environ, + "GHSTACK_TIMING": "1", + "GITHUB_TOKEN": token, + } + r = run( + [sys.executable, "-m", "ghstack", "submit", "-m", "Bench create"], + cwd=repo_dir, + env=env, + ) + if r.returncode != 0: + print( + f"ghstack submit (create) failed:\n{r.stdout}\n{r.stderr}", + file=sys.stderr, + ) + raise RuntimeError("ghstack submit failed") + create_timing = parse_timing(r.stderr) + pr_numbers = extract_pr_numbers(r.stdout) + + # Amend all commits (update PRs) + for i in range(stack_size): + fname = os.path.join(repo_dir, f"bench_{i}.txt") + with open(fname, "w") as f: + f.write(f"commit {i} updated\n") + git(repo_dir, "add", fname) + + # Amend top commit + git(repo_dir, "commit", "--amend", "--no-edit") + + r = run( + [sys.executable, "-m", "ghstack", "submit", "-m", "Bench update"], + cwd=repo_dir, + env=env, + ) + if r.returncode != 0: + print( + f"ghstack submit (update) failed:\n{r.stdout}\n{r.stderr}", + file=sys.stderr, + ) + raise RuntimeError("ghstack submit failed") + update_timing = parse_timing(r.stderr) + + # Close PRs + if pr_numbers: + close_prs(repo, token, pr_numbers) + + return create_timing, update_timing + finally: + shutil.rmtree(workdir, ignore_errors=True) + + +def main() -> None: + parser = argparse.ArgumentParser(description="Benchmark ghstack submit") + parser.add_argument("--repo", required=True, help="GitHub repo (owner/name)") + parser.add_argument( + "--token", default=os.environ.get("GITHUB_TOKEN"), help="GitHub token" + ) + parser.add_argument( + "--iterations", type=int, default=3, help="Number of iterations" + ) + parser.add_argument( + "--stack-size", type=int, default=3, help="Number of commits in stack" + ) + parser.add_argument( + "--username", default=None, help="GitHub username (auto-detected if not set)" + ) + args = parser.parse_args() + + if not args.token: + print("Error: GITHUB_TOKEN env var or --token required", file=sys.stderr) + sys.exit(1) + + if args.username is None: + import requests + + r = requests.get( + "https://api.github.com/user", + headers={"Authorization": f"token {args.token}"}, + ) + args.username = r.json()["login"] + + print(f"Benchmarking ghstack submit against {args.repo}") + print(f" Stack size: {args.stack_size}") + print(f" Iterations: {args.iterations}") + print(f" Username: {args.username}") + print() + + create_timings: List[Dict[str, float]] = [] + update_timings: List[Dict[str, float]] = [] + + for i in range(args.iterations): + print(f"Iteration {i + 1}/{args.iterations}...", end=" ", flush=True) + create, update = run_benchmark( + args.repo, args.token, args.stack_size, args.username + ) + create_timings.append(create) + update_timings.append(update) + print( + f"create={create.get('total', 0):.0f}ms update={update.get('total', 0):.0f}ms" + ) + + print() + print("=== Results (median over {} iterations) ===".format(args.iterations)) + print() + + for label, timings in [("CREATE", create_timings), ("UPDATE", update_timings)]: + print(f" {label}:") + all_keys = sorted(set(k for t in timings for k in t.keys())) + for key in all_keys: + values = [t.get(key, 0) for t in timings] + median = statistics.median(values) + print(f" {key:25s} {median:7.0f}ms") + print() + + +if __name__ == "__main__": + main() From d74cb3f5cd428366f8e3e4db94dd6e2391afc6e2 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 12:04:07 -0400 Subject: [PATCH 05/18] Update [ghstack-poisoned] --- src/ghstack/github_fake.py | 6 +++--- src/ghstack/github_real.py | 32 ++++++++++++++++++++++---------- 2 files changed, 25 insertions(+), 13 deletions(-) diff --git a/src/ghstack/github_fake.py b/src/ghstack/github_fake.py index e157445..8abeb5e 100644 --- a/src/ghstack/github_fake.py +++ b/src/ghstack/github_fake.py @@ -170,16 +170,16 @@ def __init__(self, upstream_sh: Optional[ghstack.shell.Shell]) -> None: # operations depend on repository state (e.g., what # the headRef is at the time a PR is created), so # we need this information - self.upstream_sh.git("init", "--bare", "-b", "master") + self.upstream_sh.git("init", "--bare", "-b", "main") tree = self.upstream_sh.git("write-tree") commit = self.upstream_sh.git("commit-tree", tree, input="Initial commit") - self.upstream_sh.git("branch", "-f", "master", commit) + self.upstream_sh.git("branch", "-f", "main", commit) # We only update this when a PATCH changes the default # branch; hopefully that's fine? In any case, it should # work for now since currently we only ever access the name # of the default branch rather than other parts of its ref. - repo.defaultBranchRef = repo._make_ref(self, "master") + repo.defaultBranchRef = repo._make_ref(self, "main") @dataclass diff --git a/src/ghstack/github_real.py b/src/ghstack/github_real.py index d949df4..b7d9bfc 100644 --- a/src/ghstack/github_real.py +++ b/src/ghstack/github_real.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import asyncio +import itertools import json import logging import re @@ -73,6 +74,7 @@ def __init__( self.github_url = github_url self.verify = verify self.cert = cert + self._rest_request_ids = itertools.count(1) def push_hook(self, refName: Sequence[str]) -> None: pass @@ -183,9 +185,13 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any: url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path backoff_seconds = INITIAL_BACKOFF_SECONDS + request_id = next(self._rest_request_ids) + log_prefix = f"rest[{request_id}]" for attempt in range(0, MAX_RETRIES): - logging.debug("# {} {}".format(method, url)) - logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1))) + logging.debug("# %s %s %s", log_prefix, method, url) + logging.debug( + "%s request body:\n%s", log_prefix, json.dumps(kwargs, indent=1) + ) resp: requests.Response = getattr(requests, method)( url, @@ -196,16 +202,16 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any: cert=self.cert, ) - logging.debug("Response status: {}".format(resp.status_code)) + logging.debug("%s response status: %s", log_prefix, resp.status_code) try: r = resp.json() except ValueError: - logging.debug("Response body:\n{}".format(resp.text)) + logging.debug("%s response body:\n%s", log_prefix, resp.text) raise else: pretty_json = json.dumps(r, indent=1) - logging.debug("Response JSON:\n{}".format(pretty_json)) + logging.debug("%s response JSON:\n%s", log_prefix, pretty_json) # Per Github rate limiting: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#exceeding-the-rate-limit if resp.status_code in (403, 429): @@ -280,21 +286,27 @@ async def arest(self, method: str, path: str, **kwargs: Any) -> Any: request_kwargs["ssl"] = aiohttp_ssl backoff_seconds = INITIAL_BACKOFF_SECONDS + request_id = next(self._rest_request_ids) + log_prefix = f"rest[{request_id}]" async with aiohttp.ClientSession() as session: for attempt in range(0, MAX_RETRIES): - logging.debug("# {} {}".format(method, url)) - logging.debug("Request body:\n{}".format(json.dumps(kwargs, indent=1))) + logging.debug("# %s %s %s", log_prefix, method, url) + logging.debug( + "%s request body:\n%s", log_prefix, json.dumps(kwargs, indent=1) + ) async with getattr(session, method)(url, **request_kwargs) as resp: - logging.debug("Response status: {}".format(resp.status)) + logging.debug("%s response status: %s", log_prefix, resp.status) try: r = await resp.json() except (aiohttp.ContentTypeError, ValueError): - logging.debug("Response body:\n{}".format(await resp.text())) + logging.debug( + "%s response body:\n%s", log_prefix, await resp.text() + ) raise else: pretty_json = json.dumps(r, indent=1) - logging.debug("Response JSON:\n{}".format(pretty_json)) + logging.debug("%s response JSON:\n%s", log_prefix, pretty_json) if resp.status in (403, 429): remaining_count = resp.headers.get("x-ratelimit-remaining") From 764f541edcae6d86bb6ed87b05dd99d15dd98423 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 12:08:32 -0400 Subject: [PATCH 06/18] Update [ghstack-poisoned] --- src/ghstack/github_fake.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ghstack/github_fake.py b/src/ghstack/github_fake.py index 8abeb5e..8ecd888 100644 --- a/src/ghstack/github_fake.py +++ b/src/ghstack/github_fake.py @@ -375,11 +375,17 @@ def graphql(self, query: str, **kwargs: Any) -> Any: variable_values=kwargs, ) if r.errors: + # The GraphQL implementation loses all the stack traces!!! + # D: You can 'recover' them by deleting the + # 'except Exception as error' from GraphQL-core-next; need + # to file a bug report raise RuntimeError( "GraphQL query failed with errors:\n\n{}".format( "\n".join(str(e) for e in r.errors) ) ) + # The top-level object isn't indexable by strings, but + # everything underneath is, oddly enough return {"data": r.data} def push_hook(self, refNames: Sequence[str]) -> None: From c80ecb9f1fe372b86f1147ab4d72c827597a3fb2 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 12:11:14 -0400 Subject: [PATCH 07/18] Update [ghstack-poisoned] --- src/ghstack/github.py | 8 +++- src/ghstack/github_fake.py | 88 ++---------------------------------- src/ghstack/github_real.py | 92 -------------------------------------- 3 files changed, 9 insertions(+), 179 deletions(-) diff --git a/src/ghstack/github.py b/src/ghstack/github.py index c135269..56d8a72 100644 --- a/src/ghstack/github.py +++ b/src/ghstack/github.py @@ -1,5 +1,6 @@ #!/usr/bin/env python3 +import asyncio from abc import ABCMeta, abstractmethod from typing import Any, Sequence @@ -82,7 +83,6 @@ def patch(self, path: str, **kwargs: Any) -> Any: """ return self.rest("patch", path, **kwargs) - @abstractmethod def rest(self, method: str, path: str, **kwargs: Any) -> Any: """ Send a 'method' request to endpoint 'path'. @@ -94,7 +94,11 @@ def rest(self, method: str, path: str, **kwargs: Any) -> Any: Returns: parsed JSON response """ - pass + loop = asyncio.new_event_loop() + try: + return loop.run_until_complete(self.arest(method, path, **kwargs)) + finally: + loop.close() @abstractmethod async def arest(self, method: str, path: str, **kwargs: Any) -> Any: diff --git a/src/ghstack/github_fake.py b/src/ghstack/github_fake.py index 8ecd888..b2008d5 100644 --- a/src/ghstack/github_fake.py +++ b/src/ghstack/github_fake.py @@ -170,16 +170,16 @@ def __init__(self, upstream_sh: Optional[ghstack.shell.Shell]) -> None: # operations depend on repository state (e.g., what # the headRef is at the time a PR is created), so # we need this information - self.upstream_sh.git("init", "--bare", "-b", "main") + self.upstream_sh.git("init", "--bare", "-b", "master") tree = self.upstream_sh.git("write-tree") commit = self.upstream_sh.git("commit-tree", tree, input="Initial commit") - self.upstream_sh.git("branch", "-f", "main", commit) + self.upstream_sh.git("branch", "-f", "master", commit) # We only update this when a PATCH changes the default # branch; hopefully that's fine? In any case, it should # work for now since currently we only ever access the name # of the default branch rather than other parts of its ref. - repo.defaultBranchRef = repo._make_ref(self, "main") + repo.defaultBranchRef = repo._make_ref(self, "master") @dataclass @@ -394,41 +394,6 @@ def push_hook(self, refNames: Sequence[str]) -> None: def notify_merged(self, pr_resolved: ghstack.diff.PullRequestResolved) -> None: self.state.notify_merged(pr_resolved) - def _create_pull( - self, owner: str, name: str, input: CreatePullRequestInput - ) -> CreatePullRequestPayload: - state = self.state - id = state.next_id() - repo = state.repository(owner, name) - number = state.next_pull_request_number(repo.id) - baseRef = None - headRef = None - # TODO: When we support forks, this needs rewriting to stop - # hard coded the repo we opened the pull request on - if state.upstream_sh: - baseRef = repo._make_ref(state, input["base"]) - headRef = repo._make_ref(state, input["head"]) - pr = PullRequest( - id=id, - _repository=repo.id, - number=number, - closed=False, - url="https://github.com/{}/pull/{}".format(repo.nameWithOwner, number), - baseRef=baseRef, - baseRefName=input["base"], - headRef=headRef, - headRefName=input["head"], - title=input["title"], - body=input["body"], - ) - # TODO: compute files changed - state.pull_requests[id] = pr - # This is only a subset of what the actual REST endpoint - # returns. - return { - "number": number, - } - async def _create_pull_async( self, owner: str, name: str, input: CreatePullRequestInput ) -> CreatePullRequestPayload: @@ -459,24 +424,6 @@ async def _create_pull_async( "number": number, } - # NB: This technically does have a payload, but we don't - # use it so I didn't bother constructing it. - def _update_pull( - self, owner: str, name: str, number: GitHubNumber, input: UpdatePullRequestInput - ) -> None: - state = self.state - repo = state.repository(owner, name) - pr = state.pull_request(repo, number) - # If I say input.get('title') is not None, mypy - # is unable to infer input['title'] is not None - if "title" in input and input["title"] is not None: - pr.title = input["title"] - if "base" in input and input["base"] is not None: - pr.baseRefName = input["base"] - pr.baseRef = repo._make_ref(state, pr.baseRefName) - if "body" in input and input["body"] is not None: - pr.body = input["body"] - async def _update_pull_async( self, owner: str, name: str, number: GitHubNumber, input: UpdatePullRequestInput ) -> None: @@ -520,15 +467,6 @@ def _update_issue_comment( if (r := input.get("body")) is not None: comment.body = r - # NB: This may have a payload, but we don't - # use it so I didn't bother constructing it. - def _set_default_branch( - self, owner: str, name: str, input: SetDefaultBranchInput - ) -> None: - state = self.state - repo = state.repository(owner, name) - repo.defaultBranchRef = repo._make_ref(state, input["default_branch"]) - async def _set_default_branch_async( self, owner: str, name: str, input: SetDefaultBranchInput ) -> None: @@ -538,9 +476,6 @@ async def _set_default_branch_async( state, input["default_branch"] ) - def rest(self, method: str, path: str, **kwargs: Any) -> Any: - return self._rest_impl(method, path, **kwargs) - async def arest(self, method: str, path: str, **kwargs: Any) -> Any: return await self._arest_impl(method, path, **kwargs) @@ -576,10 +511,6 @@ def _rest_impl(self, method: str, path: str, **kwargs: Any) -> Any: } elif method == "post": - if m := re.match(r"^repos/([^/]+)/([^/]+)/pulls$", path): - return self._create_pull( - m.group(1), m.group(2), cast(CreatePullRequestInput, kwargs) - ) if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/([^/]+)/comments", path): return self._create_issue_comment( m.group(1), @@ -606,19 +537,6 @@ def _rest_impl(self, method: str, path: str, **kwargs: Any) -> Any: pr.labels.extend(labels) return {} elif method == "patch": - if m := re.match(r"^repos/([^/]+)/([^/]+)(?:/pulls/([^/]+))?$", path): - owner, name, number = m.groups() - if number is not None: - return self._update_pull( - owner, - name, - GitHubNumber(int(number)), - cast(UpdatePullRequestInput, kwargs), - ) - elif "default_branch" in kwargs: - return self._set_default_branch( - owner, name, cast(SetDefaultBranchInput, kwargs) - ) if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/comments/([^/]+)$", path): return self._update_issue_comment( m.group(1), diff --git a/src/ghstack/github_real.py b/src/ghstack/github_real.py index b7d9bfc..19108b8 100644 --- a/src/ghstack/github_real.py +++ b/src/ghstack/github_real.py @@ -178,98 +178,6 @@ def _aiohttp_ssl(self) -> Any: context.load_cert_chain(self.cert) return context - def rest(self, method: str, path: str, **kwargs: Any) -> Any: - assert self.oauth_token - headers = self._rest_headers() - - url = self.rest_endpoint.format(github_url=self.github_url) + "/" + path - - backoff_seconds = INITIAL_BACKOFF_SECONDS - request_id = next(self._rest_request_ids) - log_prefix = f"rest[{request_id}]" - for attempt in range(0, MAX_RETRIES): - logging.debug("# %s %s %s", log_prefix, method, url) - logging.debug( - "%s request body:\n%s", log_prefix, json.dumps(kwargs, indent=1) - ) - - resp: requests.Response = getattr(requests, method)( - url, - json=kwargs, - headers=headers, - proxies=self._proxies(), - verify=self.verify, - cert=self.cert, - ) - - logging.debug("%s response status: %s", log_prefix, resp.status_code) - - try: - r = resp.json() - except ValueError: - logging.debug("%s response body:\n%s", log_prefix, resp.text) - raise - else: - pretty_json = json.dumps(r, indent=1) - logging.debug("%s response JSON:\n%s", log_prefix, pretty_json) - - # Per Github rate limiting: https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api?apiVersion=2022-11-28#exceeding-the-rate-limit - if resp.status_code in (403, 429): - remaining_count = resp.headers.get("x-ratelimit-remaining") - reset_time = resp.headers.get("x-ratelimit-reset") - - if remaining_count == "0" and reset_time: - sleep_time = int(reset_time) - int(time.time()) - logging.warning( - f"Rate limit exceeded. Sleeping until reset in {sleep_time} seconds." - ) - time.sleep(sleep_time) - continue - else: - retry_after_seconds = resp.headers.get("retry-after") - if retry_after_seconds: - sleep_time = int(retry_after_seconds) - logging.warning( - f"Secondary rate limit hit. Sleeping for {sleep_time} seconds." - ) - else: - sleep_time = backoff_seconds - logging.warning( - f"Secondary rate limit hit. Sleeping for {sleep_time} seconds (exponential backoff)." - ) - backoff_seconds *= 2 - time.sleep(sleep_time) - continue - - if resp.status_code == 404: - raise ghstack.github.NotFoundError( - """\ -GitHub raised a 404 error on the request for -{url}. -Usually, this doesn't actually mean the page doesn't exist; instead, it -usually means that you didn't configure your OAuth token with enough -permissions. Please create a new OAuth token at -https://{github_url}/settings/tokens and DOUBLE CHECK that you checked -"public_repo" for permissions, and update ~/.ghstackrc with your new -value. - -Another possible reason for this error is if the repository has moved -to a new location or been renamed. Check that the repository URL is -still correct. -""".format( - url=url, github_url=self.github_url - ) - ) - - try: - resp.raise_for_status() - except requests.HTTPError: - raise RuntimeError(pretty_json) - - return r - - raise RuntimeError("Exceeded maximum retries due to GitHub rate limiting") - async def arest(self, method: str, path: str, **kwargs: Any) -> Any: assert self.oauth_token headers = self._rest_headers() From f048d0dd45a726ba418e50ec4d4ff683629e843e Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 12:26:08 -0400 Subject: [PATCH 08/18] Update [ghstack-poisoned] --- test/checkout/basic.py.test | 6 ++-- test/checkout/same_base_allows.py.test | 2 +- test/checkout/same_base_rejects.py.test | 10 +++---- test/cherry_pick/basic.py.test | 8 +++--- test/cherry_pick/conflict.py.test | 6 ++-- test/cherry_pick/stack.py.test | 8 +++--- test/cherry_pick/stack_manual.py.test | 8 +++--- test/land/default_branch_change.py.test | 28 +++++++++---------- test/land/early_mod.py.test | 4 +-- test/land/ff.py.test | 2 +- test/land/ff_stack.py.test | 2 +- test/land/ff_stack_two_phase.py.test | 2 +- test/land/invalid_resubmit.py.test | 4 +-- test/land/non_ff.py.test | 4 +-- test/land/non_ff_stack_two_phase.py.test | 6 ++-- test/land/update_after_land.py.test | 6 ++-- test/log/explicit_pr.py.test | 2 +- test/submit/amend.py.test | 6 ++-- test/submit/amend_all.py.test | 6 ++-- test/submit/amend_bottom.py.test | 6 ++-- test/submit/amend_message_only.py.test | 6 ++-- test/submit/amend_top.py.test | 6 ++-- test/submit/bullet_divider.py.test | 6 ++-- test/submit/cherry_pick.py.test | 12 ++++---- test/submit/cli_reviewer_and_label.py.test | 6 ++-- test/submit/commit_amended_to_empty.py.test | 2 +- ...ot_revert_local_commit_msg_on_skip.py.test | 6 ++-- test/submit/empty_commit.py.test | 2 +- test/submit/minimal_fetch.py.test | 2 +- test/submit/multi.py.test | 6 ++-- test/submit/no_clobber.py.test | 12 ++++---- .../no_clobber_carriage_returns.py.test | 12 ++++---- test/submit/non_standard_base.py.test | 6 ++-- .../submit/preserve_downstream_closed.py.test | 2 +- .../submit/preserve_downstream_middle.py.test | 2 +- .../preserve_downstream_multiple.py.test | 2 +- .../preserve_downstream_new_on_top.py.test | 6 ++-- test/submit/preserve_downstream_prs.py.test | 2 +- .../preserve_downstream_rearrange.py.test | 2 +- test/submit/rebase.py.test | 12 ++++---- test/submit/remove_bottom_commit.py.test | 10 +++---- test/submit/reorder.py.test | 6 ++-- test/submit/reviewer_and_label.py.test | 6 ++-- test/submit/simple.py.test | 6 ++-- .../unrelated_malformed_gh_branch_ok.py.test | 8 +++--- test/submit/update_fields.py.test | 12 ++++---- ...lds_preserve_differential_revision.py.test | 12 ++++---- ...te_fields_preserves_commit_message.py.test | 6 ++-- test/unlink/basic.py.test | 8 +++--- 49 files changed, 156 insertions(+), 156 deletions(-) diff --git a/test/checkout/basic.py.test b/test/checkout/basic.py.test index e4e5cb4..06c5ab9 100644 --- a/test/checkout/basic.py.test +++ b/test/checkout/basic.py.test @@ -6,11 +6,11 @@ init_test() commit("A") (A,) = gh_submit("Initial commit") -# Move to master and create another commit -git("checkout", "master") +# Move to main and create another commit +git("checkout", "main") commit("B") -# Verify we're on master with commit B +# Verify we're on main with commit B current_log = git("log", "--oneline", "-n", "1") assert "Commit B" in current_log diff --git a/test/checkout/same_base_allows.py.test b/test/checkout/same_base_allows.py.test index ead555a..2f58ae8 100644 --- a/test/checkout/same_base_allows.py.test +++ b/test/checkout/same_base_allows.py.test @@ -12,7 +12,7 @@ assert len(diffs) == 2 A = diffs[0] # First commit (A) B = diffs[1] # Second commit (B) -# Both PRs should have the same merge-base with master (initial commit) +# Both PRs should have the same merge-base with main (initial commit) # Checkout PR A gh_checkout(f"https://github.com/pytorch/pytorch/pull/{A.number}") diff --git a/test/checkout/same_base_rejects.py.test b/test/checkout/same_base_rejects.py.test index 7c44c2d..6ee84ae 100644 --- a/test/checkout/same_base_rejects.py.test +++ b/test/checkout/same_base_rejects.py.test @@ -3,16 +3,16 @@ from ghstack.test_prelude import * init_test() -# Create first PR based on initial master +# Create first PR based on initial main commit("A") (A,) = gh_submit("First PR") -# Go back to master and advance it -git("checkout", "master") +# Go back to main and advance it +git("checkout", "main") commit("B") -git("push", "origin", "master") +git("push", "origin", "main") -# Create second PR based on new master (different merge-base) +# Create second PR based on new main (different merge-base) commit("C") (C,) = gh_submit("Second PR") diff --git a/test/cherry_pick/basic.py.test b/test/cherry_pick/basic.py.test index 1a115a8..26ef530 100644 --- a/test/cherry_pick/basic.py.test +++ b/test/cherry_pick/basic.py.test @@ -7,11 +7,11 @@ git("checkout", "-b", "feature") commit("A") (A,) = gh_submit("Initial commit") -# Switch to master and add another commit -git("checkout", "master") +# Switch to main and add another commit +git("checkout", "main") commit("M") -# Before cherry-pick, "Commit A" should NOT be in recent master history +# Before cherry-pick, "Commit A" should NOT be in recent main history log_before = git("log", "--oneline", "-n", "2") assert "Commit A" not in log_before assert "Commit M" in log_before @@ -19,7 +19,7 @@ assert "Commit M" in log_before # Now cherry-pick the PR commit gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{A.number}") -# After cherry-pick, "Commit A" SHOULD be in recent master history +# After cherry-pick, "Commit A" SHOULD be in recent main history log_after = git("log", "--oneline", "-n", "3") assert "Commit A" in log_after assert "Commit M" in log_after diff --git a/test/cherry_pick/conflict.py.test b/test/cherry_pick/conflict.py.test index 3854a78..d983e4c 100644 --- a/test/cherry_pick/conflict.py.test +++ b/test/cherry_pick/conflict.py.test @@ -7,14 +7,14 @@ git("checkout", "-b", "feature") commit("A") (A,) = gh_submit("Initial commit") -# Switch to master and modify the same file differently -git("checkout", "master") +# Switch to main and modify the same file differently +git("checkout", "main") # The commit() function creates .txt with content "A" # Let's create a conflicting change by committing to the same file write_file_and_add("A.txt", "Different content") git("commit", "-m", "Conflicting change") -# Before cherry-pick, verify "Commit A" is not in master +# Before cherry-pick, verify "Commit A" is not in main log_before = git("log", "--oneline", "-n", "2") assert "Commit A" not in log_before assert "Conflicting change" in log_before diff --git a/test/cherry_pick/stack.py.test b/test/cherry_pick/stack.py.test index f701b57..e225b63 100644 --- a/test/cherry_pick/stack.py.test +++ b/test/cherry_pick/stack.py.test @@ -8,11 +8,11 @@ commit("A") commit("B") A, B = gh_submit("Initial stack") -# Switch to master and add another commit -git("checkout", "master") +# Switch to main and add another commit +git("checkout", "main") commit("M") -# Before cherry-pick, "Commit B" should NOT be in recent master history +# Before cherry-pick, "Commit B" should NOT be in recent main history log_before = git("log", "--oneline", "-n", "2") assert "Commit B" not in log_before assert "Commit M" in log_before @@ -22,7 +22,7 @@ assert "Commit M" in log_before # in the test environment's temporary directories gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{B.number}") -# After cherry-pick, "Commit B" SHOULD be in recent master history +# After cherry-pick, "Commit B" SHOULD be in recent main history log_output = git("log", "--oneline", "-n", "3") assert "Commit B" in log_output assert "Commit M" in log_output diff --git a/test/cherry_pick/stack_manual.py.test b/test/cherry_pick/stack_manual.py.test index 0ae871d..aa13d5e 100644 --- a/test/cherry_pick/stack_manual.py.test +++ b/test/cherry_pick/stack_manual.py.test @@ -12,11 +12,11 @@ commit("C") # Create PRs for the stack A, B, C = gh_submit("Stack of 3 commits") -# Go back to master and create a divergent commit -git("checkout", "master") +# Go back to main and create a divergent commit +git("checkout", "main") commit("M") -# Before cherry-pick, "Commit C" should NOT be in master history +# Before cherry-pick, "Commit C" should NOT be in main history log_before = git("log", "--oneline", "-n", "2") assert "Commit C" not in log_before assert "Commit M" in log_before @@ -24,7 +24,7 @@ assert "Commit M" in log_before # Now test cherry-picking the top commit only (not stack) gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{C.number}") -# After cherry-pick, should only have C and M in recent master history, not A and B +# After cherry-pick, should only have C and M in recent main history, not A and B log_output = git("log", "--oneline", "-n", "4") assert "Commit C" in log_output assert "Commit M" in log_output diff --git a/test/land/default_branch_change.py.test b/test/land/default_branch_change.py.test index 7160e23..51d36c4 100644 --- a/test/land/default_branch_change.py.test +++ b/test/land/default_branch_change.py.test @@ -6,14 +6,14 @@ commit("A") (diff1,) = gh_submit("Initial 1") assert diff1 is not None -# make main branch -git("branch", "main", "master") -git("push", "origin", "main") -# change default branch to main +# make release branch +git("branch", "release", "main") +git("push", "origin", "release") +# change default branch to release get_github().patch( "repos/pytorch/pytorch", name="pytorch", - default_branch="main", + default_branch="release", ) assert_github_state( @@ -33,7 +33,7 @@ assert_github_state( | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -42,11 +42,11 @@ assert_github_state( gh_land(diff1.pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """dc8bfe4 Initial commit""", ) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + get_upstream_sh().git("log", "--oneline", "release"), """\ 8927014 Commit A dc8bfe4 Initial commit""", @@ -57,11 +57,11 @@ commit("B") (diff2,) = gh_submit("Initial 2") assert diff2 is not None -# change default branch back to master +# change default branch back to main get_github().patch( "repos/pytorch/pytorch", name="pytorch", - default_branch="master", + default_branch="main", ) assert_github_state( @@ -90,13 +90,13 @@ assert_github_state( | Initial 2 * 742ae0b (gh/ezyang/2/base) | Initial 2 (base update) - * 8927014 (main, gh/ezyang/1/orig) + * 8927014 (release, gh/ezyang/1/orig) | Commit A | * 36fcfdf (gh/ezyang/1/head) | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -105,14 +105,14 @@ assert_github_state( gh_land(diff2.pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 6b7e56e Commit B (#501) 1132c50 Commit A (#500) dc8bfe4 Initial commit""", ) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + get_upstream_sh().git("log", "--oneline", "release"), """\ 8927014 Commit A dc8bfe4 Initial commit""", diff --git a/test/land/early_mod.py.test b/test/land/early_mod.py.test index 4d00b45..089f0e1 100644 --- a/test/land/early_mod.py.test +++ b/test/land/early_mod.py.test @@ -18,5 +18,5 @@ amend("A2") gh_submit("Update") gh_land(pr_url) -assert_expected_inline(get_upstream_sh().git("show", "master:A2.txt"), """A""") -assert_expected_inline(get_upstream_sh().git("show", "master:B.txt"), """A""") +assert_expected_inline(get_upstream_sh().git("show", "main:A2.txt"), """A""") +assert_expected_inline(get_upstream_sh().git("show", "main:B.txt"), """A""") diff --git a/test/land/ff.py.test b/test/land/ff.py.test index 6e30a9d..4c771b1 100644 --- a/test/land/ff.py.test +++ b/test/land/ff.py.test @@ -9,7 +9,7 @@ pr_url = diff.pr_url gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ d518c9f Commit A (#500) dc8bfe4 Initial commit""", diff --git a/test/land/ff_stack.py.test b/test/land/ff_stack.py.test index 5460c9f..e40f11e 100644 --- a/test/land/ff_stack.py.test +++ b/test/land/ff_stack.py.test @@ -14,7 +14,7 @@ pr_url = diff2.pr_url gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 4099517 Commit B (#501) c28edd5 Commit A (#500) diff --git a/test/land/ff_stack_two_phase.py.test b/test/land/ff_stack_two_phase.py.test index d73dff2..d2c0ca6 100644 --- a/test/land/ff_stack_two_phase.py.test +++ b/test/land/ff_stack_two_phase.py.test @@ -16,7 +16,7 @@ pr_url2 = diff2.pr_url gh_land(pr_url1) gh_land(pr_url2) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 4099517 Commit B (#501) c28edd5 Commit A (#500) diff --git a/test/land/invalid_resubmit.py.test b/test/land/invalid_resubmit.py.test index 1ee1657..af7a06c 100644 --- a/test/land/invalid_resubmit.py.test +++ b/test/land/invalid_resubmit.py.test @@ -20,7 +20,7 @@ assert_expected_raises_inline( # Do the remediation gh_unlink() -git("rebase", "origin/master") +git("rebase", "origin/main") gh_submit("New PR") if is_direct(): @@ -52,7 +52,7 @@ else: | New PR * 5f392f5 (gh/ezyang/1/base) | New PR (base update) - * d518c9f (HEAD -> master) + * d518c9f (HEAD -> main) | Commit A (#500) * dc8bfe4 Initial commit diff --git a/test/land/non_ff.py.test b/test/land/non_ff.py.test index 7c9aab9..4493f4a 100644 --- a/test/land/non_ff.py.test +++ b/test/land/non_ff.py.test @@ -7,7 +7,7 @@ commit("A") assert diff is not None pr_url = diff.pr_url -git("reset", "--hard", "origin/master") +git("reset", "--hard", "origin/main") commit("U") git("push") @@ -15,7 +15,7 @@ git("checkout", "gh/ezyang/1/orig") gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 8b61aeb Commit A (#500) 38808c0 Commit U diff --git a/test/land/non_ff_stack_two_phase.py.test b/test/land/non_ff_stack_two_phase.py.test index ecc45a1..36e44dc 100644 --- a/test/land/non_ff_stack_two_phase.py.test +++ b/test/land/non_ff_stack_two_phase.py.test @@ -13,14 +13,14 @@ assert diff2 is not None pr_url1 = diff1.pr_url pr_url2 = diff2.pr_url -git("checkout", "origin/master") +git("checkout", "origin/main") commit("C") -git("push", "origin", "HEAD:master") +git("push", "origin", "HEAD:main") gh_land(pr_url1) gh_land(pr_url2) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 402e96c Commit B (#501) e388a10 Commit A (#500) diff --git a/test/land/update_after_land.py.test b/test/land/update_after_land.py.test index 88fa5c5..5e11c51 100644 --- a/test/land/update_after_land.py.test +++ b/test/land/update_after_land.py.test @@ -9,7 +9,7 @@ assert diff1 is not None assert diff2 is not None # setup an upstream commit, so the land isn't just trivial -git("reset", "--hard", "origin/master") +git("reset", "--hard", "origin/main") commit("U") git("push") @@ -30,7 +30,7 @@ assert_expected_raises_inline( ) # show the remediation works -git("rebase", "origin/master") +git("rebase", "origin/main") gh_submit("Run 3") if is_direct(): @@ -64,7 +64,7 @@ else: |\\ Run 3 | * a800ca6 (gh/ezyang/2/base) | |\\ Run 3 (base update) - | | * 70eb094 (HEAD -> master) + | | * 70eb094 (HEAD -> main) | | | Commit A (#500) | | * 7f0288c | | | Commit U diff --git a/test/log/explicit_pr.py.test b/test/log/explicit_pr.py.test index ae9450a..b7e10dc 100644 --- a/test/log/explicit_pr.py.test +++ b/test/log/explicit_pr.py.test @@ -7,7 +7,7 @@ init_test() commit("A") (A,) = gh_submit("Initial commit") -git("checkout", "master") +git("checkout", "main") with captured_output() as (out, _): gh_log( diff --git a/test/submit/amend.py.test b/test/submit/amend.py.test index f30f147..87b359b 100644 --- a/test/submit/amend.py.test +++ b/test/submit/amend.py.test @@ -11,7 +11,7 @@ amend("A2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -24,7 +24,7 @@ if is_direct(): | Update A * 8c7d059 | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -49,7 +49,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/amend_all.py.test b/test/submit/amend_all.py.test index 33ee96f..f37f8bc 100644 --- a/test/submit/amend_all.py.test +++ b/test/submit/amend_all.py.test @@ -17,7 +17,7 @@ A3, B3 = gh_submit("Update A") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -41,7 +41,7 @@ if is_direct(): |/ Initial 2 * 8c7d059 | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -86,7 +86,7 @@ else: | |/ Initial 2 | * c05297f |/ Initial 2 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/amend_bottom.py.test b/test/submit/amend_bottom.py.test index fa99094..0e08154 100644 --- a/test/submit/amend_bottom.py.test +++ b/test/submit/amend_bottom.py.test @@ -17,7 +17,7 @@ A4, B4 = gh_submit("Update B") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -41,7 +41,7 @@ if is_direct(): |/ Initial 2 * 8c7d059 | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -86,7 +86,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/amend_message_only.py.test b/test/submit/amend_message_only.py.test index c909511..d7ab89b 100644 --- a/test/submit/amend_message_only.py.test +++ b/test/submit/amend_message_only.py.test @@ -12,7 +12,7 @@ git("commit", "--amend", "-m", A.commit_msg.replace("AAA", "BBB")) if is_direct(): assert_github_state( """\ - [O] #500 Commit AAA (gh/ezyang/1/head -> master) + [O] #500 Commit AAA (gh/ezyang/1/head -> main) This is commit AAA @@ -22,7 +22,7 @@ if is_direct(): * 5cd944b (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -44,7 +44,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/amend_top.py.test b/test/submit/amend_top.py.test index d8c524a..1148d6a 100644 --- a/test/submit/amend_top.py.test +++ b/test/submit/amend_top.py.test @@ -14,7 +14,7 @@ A3, B3 = gh_submit("Update A") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -35,7 +35,7 @@ if is_direct(): | Initial 2 * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -75,7 +75,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/bullet_divider.py.test b/test/submit/bullet_divider.py.test index 14fcc2c..01dac4d 100644 --- a/test/submit/bullet_divider.py.test +++ b/test/submit/bullet_divider.py.test @@ -17,7 +17,7 @@ gh_submit("Initial") if is_direct(): assert_github_state( """\ - [O] #500 This is my commit (gh/ezyang/1/head -> master) + [O] #500 This is my commit (gh/ezyang/1/head -> main) * It starts with a fabulous * Bullet list @@ -28,7 +28,7 @@ if is_direct(): * b167219 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -53,7 +53,7 @@ else: | Initial * 11e6d4d (gh/ezyang/1/base) | Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/cherry_pick.py.test b/test/submit/cherry_pick.py.test index 950c878..5899455 100644 --- a/test/submit/cherry_pick.py.test +++ b/test/submit/cherry_pick.py.test @@ -8,9 +8,9 @@ commit("A") commit("B") A, B = gh_submit("Initial 2") -git("checkout", "master") +git("checkout", "main") commit("M") -git("push", "origin", "master") +git("push", "origin", "main") cherry_pick(B) gh_submit("Cherry pick") @@ -18,13 +18,13 @@ gh_submit("Cherry pick") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A * 2193fd2 Initial 2 - [O] #501 Commit B (gh/ezyang/2/head -> master) + [O] #501 Commit B (gh/ezyang/2/head -> main) This is commit B @@ -36,7 +36,7 @@ if is_direct(): * 6d41420 (gh/ezyang/2/next, gh/ezyang/2/head) |\\ Cherry pick - | * 7ceeaa9 (HEAD -> master) + | * 7ceeaa9 (HEAD -> main) | | Commit M * | ce2fa9b | | Initial 2 @@ -76,7 +76,7 @@ else: |\\ Cherry pick | * 8f5c855 (gh/ezyang/2/base) | |\\ Cherry pick (base update) - | | * 7ceeaa9 (HEAD -> master) + | | * 7ceeaa9 (HEAD -> main) | | | Commit M * | | ca4399f |/ / Initial 2 diff --git a/test/submit/cli_reviewer_and_label.py.test b/test/submit/cli_reviewer_and_label.py.test index 117ba2b..2a4757d 100644 --- a/test/submit/cli_reviewer_and_label.py.test +++ b/test/submit/cli_reviewer_and_label.py.test @@ -23,7 +23,7 @@ assert_eq(get_pr_labels(501), ["enhancement", "priority-high"]) if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -41,7 +41,7 @@ if is_direct(): | Add B * 9cb2ede (gh/ezyang/1/next, gh/ezyang/1/head) | Initial commit - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -78,7 +78,7 @@ else: | | Initial commit | * 5956f18 (gh/ezyang/1/base) |/ Initial commit (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/commit_amended_to_empty.py.test b/test/submit/commit_amended_to_empty.py.test index b758608..06356bd 100644 --- a/test/submit/commit_amended_to_empty.py.test +++ b/test/submit/commit_amended_to_empty.py.test @@ -32,7 +32,7 @@ if not is_direct(): | Initial * ce88e73 (gh/ezyang/1/base) | Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/do_not_revert_local_commit_msg_on_skip.py.test b/test/submit/do_not_revert_local_commit_msg_on_skip.py.test index e0918a7..a292a8d 100644 --- a/test/submit/do_not_revert_local_commit_msg_on_skip.py.test +++ b/test/submit/do_not_revert_local_commit_msg_on_skip.py.test @@ -34,7 +34,7 @@ else: if is_direct(): assert_github_state( """\ - [O] #500 Commit TO_REPLACE (gh/ezyang/1/head -> master) + [O] #500 Commit TO_REPLACE (gh/ezyang/1/head -> main) This is commit TO_REPLACE @@ -44,7 +44,7 @@ if is_direct(): * 43ce90d (gh/ezyang/1/next, gh/ezyang/1/head) | Initial - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -66,7 +66,7 @@ else: | Initial * 11e6d4d (gh/ezyang/1/base) | Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/empty_commit.py.test b/test/submit/empty_commit.py.test index 0b30b7b..8ce39d0 100644 --- a/test/submit/empty_commit.py.test +++ b/test/submit/empty_commit.py.test @@ -25,7 +25,7 @@ else: | Initial * 11e6d4d (gh/ezyang/1/base) | Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/minimal_fetch.py.test b/test/submit/minimal_fetch.py.test index 5c20a69..87cad92 100644 --- a/test/submit/minimal_fetch.py.test +++ b/test/submit/minimal_fetch.py.test @@ -6,7 +6,7 @@ init_test() git( "config", "remote.origin.fetch", - "+refs/heads/master:refs/remotes/origin/master", + "+refs/heads/main:refs/remotes/origin/main", ) commit("A") diff --git a/test/submit/multi.py.test b/test/submit/multi.py.test index 2152d1e..1cce0cc 100644 --- a/test/submit/multi.py.test +++ b/test/submit/multi.py.test @@ -7,7 +7,7 @@ A, B = gh_submit("Initial 1 and 2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -25,7 +25,7 @@ if is_direct(): | Initial 1 and 2 * 6eac261 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 and 2 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) @@ -62,7 +62,7 @@ else: | | Initial 1 and 2 | * 954b129 (gh/ezyang/2/base) |/ Initial 1 and 2 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/no_clobber.py.test b/test/submit/no_clobber.py.test index c68ad71..735751d 100644 --- a/test/submit/no_clobber.py.test +++ b/test/submit/no_clobber.py.test @@ -23,7 +23,7 @@ Directly updated message body""", if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) Stack: * **#500 Commit 1** @@ -36,7 +36,7 @@ if is_direct(): * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -58,7 +58,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -72,7 +72,7 @@ tick() if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) Stack: * __->__ #500 @@ -88,7 +88,7 @@ if is_direct(): | Update 1 * 6c1d876 | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -113,7 +113,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/no_clobber_carriage_returns.py.test b/test/submit/no_clobber_carriage_returns.py.test index cda53f2..824c562 100644 --- a/test/submit/no_clobber_carriage_returns.py.test +++ b/test/submit/no_clobber_carriage_returns.py.test @@ -14,7 +14,7 @@ tick() if is_direct(): assert_github_state( """\ - [O] #500 Commit 1 (gh/ezyang/1/head -> master) + [O] #500 Commit 1 (gh/ezyang/1/head -> main) Original message @@ -24,7 +24,7 @@ if is_direct(): * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -46,7 +46,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -72,7 +72,7 @@ tick() if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) Stack: * #501 @@ -94,7 +94,7 @@ if is_direct(): | Initial 2 * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -131,7 +131,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/non_standard_base.py.test b/test/submit/non_standard_base.py.test index f78427b..a0f5820 100644 --- a/test/submit/non_standard_base.py.test +++ b/test/submit/non_standard_base.py.test @@ -3,12 +3,12 @@ from ghstack.test_prelude import * init_test() # make release branch -git("branch", "release", "master") +git("branch", "release", "main") # diverge release and regular branch -git("checkout", "master") +git("checkout", "main") commit("M") -git("push", "origin", "master") +git("push", "origin", "main") git("checkout", "release") commit("R") diff --git a/test/submit/preserve_downstream_closed.py.test b/test/submit/preserve_downstream_closed.py.test index 1c4d514..b96c6f0 100644 --- a/test/submit/preserve_downstream_closed.py.test +++ b/test/submit/preserve_downstream_closed.py.test @@ -60,7 +60,7 @@ else: | | Initial | * 8e6f9ba (gh/ezyang/2/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_middle.py.test b/test/submit/preserve_downstream_middle.py.test index 8bb20e6..173ade9 100644 --- a/test/submit/preserve_downstream_middle.py.test +++ b/test/submit/preserve_downstream_middle.py.test @@ -73,7 +73,7 @@ else: | | Initial | * 8fe2454 (gh/ezyang/3/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_multiple.py.test b/test/submit/preserve_downstream_multiple.py.test index 0851812..6472fb2 100644 --- a/test/submit/preserve_downstream_multiple.py.test +++ b/test/submit/preserve_downstream_multiple.py.test @@ -72,7 +72,7 @@ if not is_direct(): | | Initial | * 8fe2454 (gh/ezyang/3/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_new_on_top.py.test b/test/submit/preserve_downstream_new_on_top.py.test index cf0e9bf..63e4327 100644 --- a/test/submit/preserve_downstream_new_on_top.py.test +++ b/test/submit/preserve_downstream_new_on_top.py.test @@ -20,7 +20,7 @@ gh_submit("Add C", revs=["HEAD~", "HEAD"], stack=False) if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -57,7 +57,7 @@ if is_direct(): | Initial * a6cb153 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -131,7 +131,7 @@ else: | | Initial | * 8fe2454 (gh/ezyang/3/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_prs.py.test b/test/submit/preserve_downstream_prs.py.test index eee0cd4..f1f2693 100644 --- a/test/submit/preserve_downstream_prs.py.test +++ b/test/submit/preserve_downstream_prs.py.test @@ -54,7 +54,7 @@ else: | | Initial | * 8e6f9ba (gh/ezyang/2/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_rearrange.py.test b/test/submit/preserve_downstream_rearrange.py.test index b8d0996..791622f 100644 --- a/test/submit/preserve_downstream_rearrange.py.test +++ b/test/submit/preserve_downstream_rearrange.py.test @@ -79,7 +79,7 @@ else: | | Initial | * 8fe2454 (gh/ezyang/3/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/rebase.py.test b/test/submit/rebase.py.test index 3d040d3..1bd001d 100644 --- a/test/submit/rebase.py.test +++ b/test/submit/rebase.py.test @@ -7,19 +7,19 @@ commit("A") commit("B") A2, B2 = gh_submit("Initial 2") -git("checkout", "master") +git("checkout", "main") commit("M") -git("push", "origin", "master") +git("push", "origin", "main") git("checkout", "feature") -git("rebase", "origin/master") +git("rebase", "origin/main") gh_submit("Rebase") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -39,7 +39,7 @@ if is_direct(): |\\ Rebase | * b0558d8 (gh/ezyang/1/next, gh/ezyang/1/head) | |\\ Rebase - | | * 7ceeaa9 (HEAD -> master) + | | * 7ceeaa9 (HEAD -> main) | | | Commit M * | | 0243aa2 |/ / Initial 2 @@ -90,7 +90,7 @@ else: | | | |\\ Rebase (base update) | | |_|/ | |/| | - | * | | 7ceeaa9 (HEAD -> master) + | * | | 7ceeaa9 (HEAD -> main) |/ / / Commit M | * / ca4399f | |/ Initial 2 diff --git a/test/submit/remove_bottom_commit.py.test b/test/submit/remove_bottom_commit.py.test index 331d3e8..fc13471 100644 --- a/test/submit/remove_bottom_commit.py.test +++ b/test/submit/remove_bottom_commit.py.test @@ -10,20 +10,20 @@ commit("A") commit("B") A, B = gh_submit("Initial 2") -git("checkout", "master") +git("checkout", "main") cherry_pick(B) (B2,) = gh_submit("Cherry pick") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A * 2193fd2 Initial 2 - [O] #501 Commit B (gh/ezyang/2/head -> master) + [O] #501 Commit B (gh/ezyang/2/head -> main) This is commit B @@ -39,7 +39,7 @@ if is_direct(): | Initial 2 * 2193fd2 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 2 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -81,7 +81,7 @@ else: | | Initial 2 | * f081adc (gh/ezyang/1/base) |/ Initial 2 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/reorder.py.test b/test/submit/reorder.py.test index c3b2a8b..7f8f123 100644 --- a/test/submit/reorder.py.test +++ b/test/submit/reorder.py.test @@ -20,7 +20,7 @@ if is_direct(): * f61b335 Reorder - [O] #501 Commit B (gh/ezyang/2/head -> master) + [O] #501 Commit B (gh/ezyang/2/head -> main) This is commit B @@ -38,7 +38,7 @@ if is_direct(): |/ Initial * 92a6dc7 | Initial - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -85,7 +85,7 @@ else: | |/ Initial | * 8e6f9ba |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/reviewer_and_label.py.test b/test/submit/reviewer_and_label.py.test index 57fe7f4..e49374a 100644 --- a/test/submit/reviewer_and_label.py.test +++ b/test/submit/reviewer_and_label.py.test @@ -14,7 +14,7 @@ assert_eq(get_pr_labels(500), ["bug", "enhancement"]) if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -24,7 +24,7 @@ if is_direct(): * 9cb2ede (gh/ezyang/1/next, gh/ezyang/1/head) | Initial commit - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -46,7 +46,7 @@ else: | Initial commit * 5956f18 (gh/ezyang/1/base) | Initial commit (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/simple.py.test b/test/submit/simple.py.test index 636358b..83711b1 100644 --- a/test/submit/simple.py.test +++ b/test/submit/simple.py.test @@ -13,7 +13,7 @@ gh_submit("Initial 2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -31,7 +31,7 @@ if is_direct(): | Initial 2 * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -68,7 +68,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/unrelated_malformed_gh_branch_ok.py.test b/test/submit/unrelated_malformed_gh_branch_ok.py.test index 165a12c..0209a48 100644 --- a/test/submit/unrelated_malformed_gh_branch_ok.py.test +++ b/test/submit/unrelated_malformed_gh_branch_ok.py.test @@ -8,7 +8,7 @@ git("checkout", "-b", "gh/ezyang/malform") git("push", "origin", "gh/ezyang/malform") git("checkout", "-b", "gh/ezyang/non_int/head") git("push", "origin", "gh/ezyang/non_int/head") -git("checkout", "master") +git("checkout", "main") commit("A") (A,) = gh_submit("Initial 1") @@ -21,7 +21,7 @@ gh_submit("Initial 2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -39,7 +39,7 @@ if is_direct(): | Initial 2 * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master, gh/ezyang/non_int/head, gh/ezyang/malform) + * dc8bfe4 (HEAD -> main, gh/ezyang/non_int/head, gh/ezyang/malform) Initial commit """ ) @@ -76,7 +76,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master, gh/ezyang/non_int/head, gh/ezyang/malform) + * dc8bfe4 (HEAD -> main, gh/ezyang/non_int/head, gh/ezyang/malform) Initial commit """, ) diff --git a/test/submit/update_fields.py.test b/test/submit/update_fields.py.test index ea7ed55..b10de45 100644 --- a/test/submit/update_fields.py.test +++ b/test/submit/update_fields.py.test @@ -19,7 +19,7 @@ get_github().patch( if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) Directly updated message body @@ -29,7 +29,7 @@ if is_direct(): * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -48,7 +48,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -58,7 +58,7 @@ gh_submit("Update 1", update_fields=True) if is_direct(): assert_github_state( """\ - [O] #500 Commit 1 (gh/ezyang/1/head -> master) + [O] #500 Commit 1 (gh/ezyang/1/head -> main) Original message @@ -68,7 +68,7 @@ if is_direct(): * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -90,7 +90,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/update_fields_preserve_differential_revision.py.test b/test/submit/update_fields_preserve_differential_revision.py.test index a06aa89..321bb72 100644 --- a/test/submit/update_fields_preserve_differential_revision.py.test +++ b/test/submit/update_fields_preserve_differential_revision.py.test @@ -19,7 +19,7 @@ get_github().patch( if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) @@ -34,7 +34,7 @@ if is_direct(): * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -58,7 +58,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -68,7 +68,7 @@ gh_submit("Update 1", update_fields=True) if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -80,7 +80,7 @@ if is_direct(): * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -104,7 +104,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/update_fields_preserves_commit_message.py.test b/test/submit/update_fields_preserves_commit_message.py.test index 84d0949..1300639 100644 --- a/test/submit/update_fields_preserves_commit_message.py.test +++ b/test/submit/update_fields_preserves_commit_message.py.test @@ -13,7 +13,7 @@ git("commit", "--amend", "-m", "Amended " + A.commit_msg) if is_direct(): assert_github_state( """\ - [O] #500 Amended Commit A (gh/ezyang/1/head -> master) + [O] #500 Amended Commit A (gh/ezyang/1/head -> main) This is commit A @@ -23,7 +23,7 @@ if is_direct(): * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -45,7 +45,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/unlink/basic.py.test b/test/unlink/basic.py.test index 2484051..895273a 100644 --- a/test/unlink/basic.py.test +++ b/test/unlink/basic.py.test @@ -14,7 +14,7 @@ gh_submit("Initial 2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -26,7 +26,7 @@ if is_direct(): * 30b9f2a Initial 1 - [O] #502 Commit A (gh/ezyang/3/head -> master) + [O] #502 Commit A (gh/ezyang/3/head -> main) This is commit A @@ -54,7 +54,7 @@ if is_direct(): | | Initial 2 | * 2193fd2 (gh/ezyang/3/next, gh/ezyang/3/head) |/ Initial 2 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -119,7 +119,7 @@ else: | | Initial 2 | * c05297f (gh/ezyang/4/base) |/ Initial 2 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) From 68a19c799940f6636cc3706f58dda32243c00072 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 12:49:48 -0400 Subject: [PATCH 09/18] Update [ghstack-poisoned] --- test/checkout/basic.py.test | 6 ++-- test/checkout/same_base_allows.py.test | 2 +- test/checkout/same_base_rejects.py.test | 10 +++---- test/cherry_pick/basic.py.test | 8 +++--- test/cherry_pick/conflict.py.test | 6 ++-- test/cherry_pick/stack.py.test | 8 +++--- test/cherry_pick/stack_manual.py.test | 8 +++--- test/land/default_branch_change.py.test | 28 +++++++++---------- test/land/early_mod.py.test | 4 +-- test/land/ff.py.test | 2 +- test/land/ff_stack.py.test | 2 +- test/land/ff_stack_two_phase.py.test | 2 +- test/land/invalid_resubmit.py.test | 4 +-- test/land/non_ff.py.test | 4 +-- test/land/non_ff_stack_two_phase.py.test | 6 ++-- test/land/update_after_land.py.test | 6 ++-- test/log/explicit_pr.py.test | 2 +- test/submit/amend.py.test | 6 ++-- test/submit/amend_all.py.test | 6 ++-- test/submit/amend_bottom.py.test | 6 ++-- test/submit/amend_message_only.py.test | 6 ++-- test/submit/amend_top.py.test | 6 ++-- test/submit/bullet_divider.py.test | 6 ++-- test/submit/cherry_pick.py.test | 12 ++++---- test/submit/cli_reviewer_and_label.py.test | 6 ++-- test/submit/commit_amended_to_empty.py.test | 2 +- ...ot_revert_local_commit_msg_on_skip.py.test | 6 ++-- test/submit/empty_commit.py.test | 2 +- test/submit/minimal_fetch.py.test | 2 +- test/submit/multi.py.test | 6 ++-- test/submit/no_clobber.py.test | 12 ++++---- .../no_clobber_carriage_returns.py.test | 12 ++++---- test/submit/non_standard_base.py.test | 6 ++-- .../submit/preserve_downstream_closed.py.test | 2 +- .../submit/preserve_downstream_middle.py.test | 2 +- .../preserve_downstream_multiple.py.test | 2 +- .../preserve_downstream_new_on_top.py.test | 6 ++-- test/submit/preserve_downstream_prs.py.test | 2 +- .../preserve_downstream_rearrange.py.test | 2 +- test/submit/rebase.py.test | 12 ++++---- test/submit/remove_bottom_commit.py.test | 10 +++---- test/submit/reorder.py.test | 6 ++-- test/submit/reviewer_and_label.py.test | 6 ++-- test/submit/simple.py.test | 6 ++-- .../unrelated_malformed_gh_branch_ok.py.test | 8 +++--- test/submit/update_fields.py.test | 12 ++++---- ...lds_preserve_differential_revision.py.test | 12 ++++---- ...te_fields_preserves_commit_message.py.test | 6 ++-- test/unlink/basic.py.test | 8 +++--- 49 files changed, 156 insertions(+), 156 deletions(-) diff --git a/test/checkout/basic.py.test b/test/checkout/basic.py.test index e4e5cb4..06c5ab9 100644 --- a/test/checkout/basic.py.test +++ b/test/checkout/basic.py.test @@ -6,11 +6,11 @@ init_test() commit("A") (A,) = gh_submit("Initial commit") -# Move to master and create another commit -git("checkout", "master") +# Move to main and create another commit +git("checkout", "main") commit("B") -# Verify we're on master with commit B +# Verify we're on main with commit B current_log = git("log", "--oneline", "-n", "1") assert "Commit B" in current_log diff --git a/test/checkout/same_base_allows.py.test b/test/checkout/same_base_allows.py.test index ead555a..2f58ae8 100644 --- a/test/checkout/same_base_allows.py.test +++ b/test/checkout/same_base_allows.py.test @@ -12,7 +12,7 @@ assert len(diffs) == 2 A = diffs[0] # First commit (A) B = diffs[1] # Second commit (B) -# Both PRs should have the same merge-base with master (initial commit) +# Both PRs should have the same merge-base with main (initial commit) # Checkout PR A gh_checkout(f"https://github.com/pytorch/pytorch/pull/{A.number}") diff --git a/test/checkout/same_base_rejects.py.test b/test/checkout/same_base_rejects.py.test index 7c44c2d..6ee84ae 100644 --- a/test/checkout/same_base_rejects.py.test +++ b/test/checkout/same_base_rejects.py.test @@ -3,16 +3,16 @@ from ghstack.test_prelude import * init_test() -# Create first PR based on initial master +# Create first PR based on initial main commit("A") (A,) = gh_submit("First PR") -# Go back to master and advance it -git("checkout", "master") +# Go back to main and advance it +git("checkout", "main") commit("B") -git("push", "origin", "master") +git("push", "origin", "main") -# Create second PR based on new master (different merge-base) +# Create second PR based on new main (different merge-base) commit("C") (C,) = gh_submit("Second PR") diff --git a/test/cherry_pick/basic.py.test b/test/cherry_pick/basic.py.test index 1a115a8..26ef530 100644 --- a/test/cherry_pick/basic.py.test +++ b/test/cherry_pick/basic.py.test @@ -7,11 +7,11 @@ git("checkout", "-b", "feature") commit("A") (A,) = gh_submit("Initial commit") -# Switch to master and add another commit -git("checkout", "master") +# Switch to main and add another commit +git("checkout", "main") commit("M") -# Before cherry-pick, "Commit A" should NOT be in recent master history +# Before cherry-pick, "Commit A" should NOT be in recent main history log_before = git("log", "--oneline", "-n", "2") assert "Commit A" not in log_before assert "Commit M" in log_before @@ -19,7 +19,7 @@ assert "Commit M" in log_before # Now cherry-pick the PR commit gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{A.number}") -# After cherry-pick, "Commit A" SHOULD be in recent master history +# After cherry-pick, "Commit A" SHOULD be in recent main history log_after = git("log", "--oneline", "-n", "3") assert "Commit A" in log_after assert "Commit M" in log_after diff --git a/test/cherry_pick/conflict.py.test b/test/cherry_pick/conflict.py.test index 3854a78..d983e4c 100644 --- a/test/cherry_pick/conflict.py.test +++ b/test/cherry_pick/conflict.py.test @@ -7,14 +7,14 @@ git("checkout", "-b", "feature") commit("A") (A,) = gh_submit("Initial commit") -# Switch to master and modify the same file differently -git("checkout", "master") +# Switch to main and modify the same file differently +git("checkout", "main") # The commit() function creates .txt with content "A" # Let's create a conflicting change by committing to the same file write_file_and_add("A.txt", "Different content") git("commit", "-m", "Conflicting change") -# Before cherry-pick, verify "Commit A" is not in master +# Before cherry-pick, verify "Commit A" is not in main log_before = git("log", "--oneline", "-n", "2") assert "Commit A" not in log_before assert "Conflicting change" in log_before diff --git a/test/cherry_pick/stack.py.test b/test/cherry_pick/stack.py.test index f701b57..e225b63 100644 --- a/test/cherry_pick/stack.py.test +++ b/test/cherry_pick/stack.py.test @@ -8,11 +8,11 @@ commit("A") commit("B") A, B = gh_submit("Initial stack") -# Switch to master and add another commit -git("checkout", "master") +# Switch to main and add another commit +git("checkout", "main") commit("M") -# Before cherry-pick, "Commit B" should NOT be in recent master history +# Before cherry-pick, "Commit B" should NOT be in recent main history log_before = git("log", "--oneline", "-n", "2") assert "Commit B" not in log_before assert "Commit M" in log_before @@ -22,7 +22,7 @@ assert "Commit M" in log_before # in the test environment's temporary directories gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{B.number}") -# After cherry-pick, "Commit B" SHOULD be in recent master history +# After cherry-pick, "Commit B" SHOULD be in recent main history log_output = git("log", "--oneline", "-n", "3") assert "Commit B" in log_output assert "Commit M" in log_output diff --git a/test/cherry_pick/stack_manual.py.test b/test/cherry_pick/stack_manual.py.test index 0ae871d..aa13d5e 100644 --- a/test/cherry_pick/stack_manual.py.test +++ b/test/cherry_pick/stack_manual.py.test @@ -12,11 +12,11 @@ commit("C") # Create PRs for the stack A, B, C = gh_submit("Stack of 3 commits") -# Go back to master and create a divergent commit -git("checkout", "master") +# Go back to main and create a divergent commit +git("checkout", "main") commit("M") -# Before cherry-pick, "Commit C" should NOT be in master history +# Before cherry-pick, "Commit C" should NOT be in main history log_before = git("log", "--oneline", "-n", "2") assert "Commit C" not in log_before assert "Commit M" in log_before @@ -24,7 +24,7 @@ assert "Commit M" in log_before # Now test cherry-picking the top commit only (not stack) gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{C.number}") -# After cherry-pick, should only have C and M in recent master history, not A and B +# After cherry-pick, should only have C and M in recent main history, not A and B log_output = git("log", "--oneline", "-n", "4") assert "Commit C" in log_output assert "Commit M" in log_output diff --git a/test/land/default_branch_change.py.test b/test/land/default_branch_change.py.test index 7160e23..51d36c4 100644 --- a/test/land/default_branch_change.py.test +++ b/test/land/default_branch_change.py.test @@ -6,14 +6,14 @@ commit("A") (diff1,) = gh_submit("Initial 1") assert diff1 is not None -# make main branch -git("branch", "main", "master") -git("push", "origin", "main") -# change default branch to main +# make release branch +git("branch", "release", "main") +git("push", "origin", "release") +# change default branch to release get_github().patch( "repos/pytorch/pytorch", name="pytorch", - default_branch="main", + default_branch="release", ) assert_github_state( @@ -33,7 +33,7 @@ assert_github_state( | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -42,11 +42,11 @@ assert_github_state( gh_land(diff1.pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """dc8bfe4 Initial commit""", ) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + get_upstream_sh().git("log", "--oneline", "release"), """\ 8927014 Commit A dc8bfe4 Initial commit""", @@ -57,11 +57,11 @@ commit("B") (diff2,) = gh_submit("Initial 2") assert diff2 is not None -# change default branch back to master +# change default branch back to main get_github().patch( "repos/pytorch/pytorch", name="pytorch", - default_branch="master", + default_branch="main", ) assert_github_state( @@ -90,13 +90,13 @@ assert_github_state( | Initial 2 * 742ae0b (gh/ezyang/2/base) | Initial 2 (base update) - * 8927014 (main, gh/ezyang/1/orig) + * 8927014 (release, gh/ezyang/1/orig) | Commit A | * 36fcfdf (gh/ezyang/1/head) | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -105,14 +105,14 @@ assert_github_state( gh_land(diff2.pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 6b7e56e Commit B (#501) 1132c50 Commit A (#500) dc8bfe4 Initial commit""", ) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + get_upstream_sh().git("log", "--oneline", "release"), """\ 8927014 Commit A dc8bfe4 Initial commit""", diff --git a/test/land/early_mod.py.test b/test/land/early_mod.py.test index 4d00b45..089f0e1 100644 --- a/test/land/early_mod.py.test +++ b/test/land/early_mod.py.test @@ -18,5 +18,5 @@ amend("A2") gh_submit("Update") gh_land(pr_url) -assert_expected_inline(get_upstream_sh().git("show", "master:A2.txt"), """A""") -assert_expected_inline(get_upstream_sh().git("show", "master:B.txt"), """A""") +assert_expected_inline(get_upstream_sh().git("show", "main:A2.txt"), """A""") +assert_expected_inline(get_upstream_sh().git("show", "main:B.txt"), """A""") diff --git a/test/land/ff.py.test b/test/land/ff.py.test index 6e30a9d..4c771b1 100644 --- a/test/land/ff.py.test +++ b/test/land/ff.py.test @@ -9,7 +9,7 @@ pr_url = diff.pr_url gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ d518c9f Commit A (#500) dc8bfe4 Initial commit""", diff --git a/test/land/ff_stack.py.test b/test/land/ff_stack.py.test index 5460c9f..e40f11e 100644 --- a/test/land/ff_stack.py.test +++ b/test/land/ff_stack.py.test @@ -14,7 +14,7 @@ pr_url = diff2.pr_url gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 4099517 Commit B (#501) c28edd5 Commit A (#500) diff --git a/test/land/ff_stack_two_phase.py.test b/test/land/ff_stack_two_phase.py.test index d73dff2..d2c0ca6 100644 --- a/test/land/ff_stack_two_phase.py.test +++ b/test/land/ff_stack_two_phase.py.test @@ -16,7 +16,7 @@ pr_url2 = diff2.pr_url gh_land(pr_url1) gh_land(pr_url2) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 4099517 Commit B (#501) c28edd5 Commit A (#500) diff --git a/test/land/invalid_resubmit.py.test b/test/land/invalid_resubmit.py.test index 1ee1657..af7a06c 100644 --- a/test/land/invalid_resubmit.py.test +++ b/test/land/invalid_resubmit.py.test @@ -20,7 +20,7 @@ assert_expected_raises_inline( # Do the remediation gh_unlink() -git("rebase", "origin/master") +git("rebase", "origin/main") gh_submit("New PR") if is_direct(): @@ -52,7 +52,7 @@ else: | New PR * 5f392f5 (gh/ezyang/1/base) | New PR (base update) - * d518c9f (HEAD -> master) + * d518c9f (HEAD -> main) | Commit A (#500) * dc8bfe4 Initial commit diff --git a/test/land/non_ff.py.test b/test/land/non_ff.py.test index 7c9aab9..4493f4a 100644 --- a/test/land/non_ff.py.test +++ b/test/land/non_ff.py.test @@ -7,7 +7,7 @@ commit("A") assert diff is not None pr_url = diff.pr_url -git("reset", "--hard", "origin/master") +git("reset", "--hard", "origin/main") commit("U") git("push") @@ -15,7 +15,7 @@ git("checkout", "gh/ezyang/1/orig") gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 8b61aeb Commit A (#500) 38808c0 Commit U diff --git a/test/land/non_ff_stack_two_phase.py.test b/test/land/non_ff_stack_two_phase.py.test index ecc45a1..36e44dc 100644 --- a/test/land/non_ff_stack_two_phase.py.test +++ b/test/land/non_ff_stack_two_phase.py.test @@ -13,14 +13,14 @@ assert diff2 is not None pr_url1 = diff1.pr_url pr_url2 = diff2.pr_url -git("checkout", "origin/master") +git("checkout", "origin/main") commit("C") -git("push", "origin", "HEAD:master") +git("push", "origin", "HEAD:main") gh_land(pr_url1) gh_land(pr_url2) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "master"), + get_upstream_sh().git("log", "--oneline", "main"), """\ 402e96c Commit B (#501) e388a10 Commit A (#500) diff --git a/test/land/update_after_land.py.test b/test/land/update_after_land.py.test index 88fa5c5..5e11c51 100644 --- a/test/land/update_after_land.py.test +++ b/test/land/update_after_land.py.test @@ -9,7 +9,7 @@ assert diff1 is not None assert diff2 is not None # setup an upstream commit, so the land isn't just trivial -git("reset", "--hard", "origin/master") +git("reset", "--hard", "origin/main") commit("U") git("push") @@ -30,7 +30,7 @@ assert_expected_raises_inline( ) # show the remediation works -git("rebase", "origin/master") +git("rebase", "origin/main") gh_submit("Run 3") if is_direct(): @@ -64,7 +64,7 @@ else: |\\ Run 3 | * a800ca6 (gh/ezyang/2/base) | |\\ Run 3 (base update) - | | * 70eb094 (HEAD -> master) + | | * 70eb094 (HEAD -> main) | | | Commit A (#500) | | * 7f0288c | | | Commit U diff --git a/test/log/explicit_pr.py.test b/test/log/explicit_pr.py.test index ae9450a..b7e10dc 100644 --- a/test/log/explicit_pr.py.test +++ b/test/log/explicit_pr.py.test @@ -7,7 +7,7 @@ init_test() commit("A") (A,) = gh_submit("Initial commit") -git("checkout", "master") +git("checkout", "main") with captured_output() as (out, _): gh_log( diff --git a/test/submit/amend.py.test b/test/submit/amend.py.test index f30f147..87b359b 100644 --- a/test/submit/amend.py.test +++ b/test/submit/amend.py.test @@ -11,7 +11,7 @@ amend("A2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -24,7 +24,7 @@ if is_direct(): | Update A * 8c7d059 | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -49,7 +49,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/amend_all.py.test b/test/submit/amend_all.py.test index 33ee96f..f37f8bc 100644 --- a/test/submit/amend_all.py.test +++ b/test/submit/amend_all.py.test @@ -17,7 +17,7 @@ A3, B3 = gh_submit("Update A") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -41,7 +41,7 @@ if is_direct(): |/ Initial 2 * 8c7d059 | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -86,7 +86,7 @@ else: | |/ Initial 2 | * c05297f |/ Initial 2 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/amend_bottom.py.test b/test/submit/amend_bottom.py.test index fa99094..0e08154 100644 --- a/test/submit/amend_bottom.py.test +++ b/test/submit/amend_bottom.py.test @@ -17,7 +17,7 @@ A4, B4 = gh_submit("Update B") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -41,7 +41,7 @@ if is_direct(): |/ Initial 2 * 8c7d059 | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -86,7 +86,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/amend_message_only.py.test b/test/submit/amend_message_only.py.test index c909511..d7ab89b 100644 --- a/test/submit/amend_message_only.py.test +++ b/test/submit/amend_message_only.py.test @@ -12,7 +12,7 @@ git("commit", "--amend", "-m", A.commit_msg.replace("AAA", "BBB")) if is_direct(): assert_github_state( """\ - [O] #500 Commit AAA (gh/ezyang/1/head -> master) + [O] #500 Commit AAA (gh/ezyang/1/head -> main) This is commit AAA @@ -22,7 +22,7 @@ if is_direct(): * 5cd944b (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -44,7 +44,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/amend_top.py.test b/test/submit/amend_top.py.test index d8c524a..1148d6a 100644 --- a/test/submit/amend_top.py.test +++ b/test/submit/amend_top.py.test @@ -14,7 +14,7 @@ A3, B3 = gh_submit("Update A") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -35,7 +35,7 @@ if is_direct(): | Initial 2 * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -75,7 +75,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/bullet_divider.py.test b/test/submit/bullet_divider.py.test index 14fcc2c..01dac4d 100644 --- a/test/submit/bullet_divider.py.test +++ b/test/submit/bullet_divider.py.test @@ -17,7 +17,7 @@ gh_submit("Initial") if is_direct(): assert_github_state( """\ - [O] #500 This is my commit (gh/ezyang/1/head -> master) + [O] #500 This is my commit (gh/ezyang/1/head -> main) * It starts with a fabulous * Bullet list @@ -28,7 +28,7 @@ if is_direct(): * b167219 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -53,7 +53,7 @@ else: | Initial * 11e6d4d (gh/ezyang/1/base) | Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/cherry_pick.py.test b/test/submit/cherry_pick.py.test index 950c878..5899455 100644 --- a/test/submit/cherry_pick.py.test +++ b/test/submit/cherry_pick.py.test @@ -8,9 +8,9 @@ commit("A") commit("B") A, B = gh_submit("Initial 2") -git("checkout", "master") +git("checkout", "main") commit("M") -git("push", "origin", "master") +git("push", "origin", "main") cherry_pick(B) gh_submit("Cherry pick") @@ -18,13 +18,13 @@ gh_submit("Cherry pick") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A * 2193fd2 Initial 2 - [O] #501 Commit B (gh/ezyang/2/head -> master) + [O] #501 Commit B (gh/ezyang/2/head -> main) This is commit B @@ -36,7 +36,7 @@ if is_direct(): * 6d41420 (gh/ezyang/2/next, gh/ezyang/2/head) |\\ Cherry pick - | * 7ceeaa9 (HEAD -> master) + | * 7ceeaa9 (HEAD -> main) | | Commit M * | ce2fa9b | | Initial 2 @@ -76,7 +76,7 @@ else: |\\ Cherry pick | * 8f5c855 (gh/ezyang/2/base) | |\\ Cherry pick (base update) - | | * 7ceeaa9 (HEAD -> master) + | | * 7ceeaa9 (HEAD -> main) | | | Commit M * | | ca4399f |/ / Initial 2 diff --git a/test/submit/cli_reviewer_and_label.py.test b/test/submit/cli_reviewer_and_label.py.test index 117ba2b..2a4757d 100644 --- a/test/submit/cli_reviewer_and_label.py.test +++ b/test/submit/cli_reviewer_and_label.py.test @@ -23,7 +23,7 @@ assert_eq(get_pr_labels(501), ["enhancement", "priority-high"]) if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -41,7 +41,7 @@ if is_direct(): | Add B * 9cb2ede (gh/ezyang/1/next, gh/ezyang/1/head) | Initial commit - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -78,7 +78,7 @@ else: | | Initial commit | * 5956f18 (gh/ezyang/1/base) |/ Initial commit (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/commit_amended_to_empty.py.test b/test/submit/commit_amended_to_empty.py.test index b758608..06356bd 100644 --- a/test/submit/commit_amended_to_empty.py.test +++ b/test/submit/commit_amended_to_empty.py.test @@ -32,7 +32,7 @@ if not is_direct(): | Initial * ce88e73 (gh/ezyang/1/base) | Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/do_not_revert_local_commit_msg_on_skip.py.test b/test/submit/do_not_revert_local_commit_msg_on_skip.py.test index e0918a7..a292a8d 100644 --- a/test/submit/do_not_revert_local_commit_msg_on_skip.py.test +++ b/test/submit/do_not_revert_local_commit_msg_on_skip.py.test @@ -34,7 +34,7 @@ else: if is_direct(): assert_github_state( """\ - [O] #500 Commit TO_REPLACE (gh/ezyang/1/head -> master) + [O] #500 Commit TO_REPLACE (gh/ezyang/1/head -> main) This is commit TO_REPLACE @@ -44,7 +44,7 @@ if is_direct(): * 43ce90d (gh/ezyang/1/next, gh/ezyang/1/head) | Initial - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -66,7 +66,7 @@ else: | Initial * 11e6d4d (gh/ezyang/1/base) | Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/empty_commit.py.test b/test/submit/empty_commit.py.test index 0b30b7b..8ce39d0 100644 --- a/test/submit/empty_commit.py.test +++ b/test/submit/empty_commit.py.test @@ -25,7 +25,7 @@ else: | Initial * 11e6d4d (gh/ezyang/1/base) | Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/minimal_fetch.py.test b/test/submit/minimal_fetch.py.test index 5c20a69..87cad92 100644 --- a/test/submit/minimal_fetch.py.test +++ b/test/submit/minimal_fetch.py.test @@ -6,7 +6,7 @@ init_test() git( "config", "remote.origin.fetch", - "+refs/heads/master:refs/remotes/origin/master", + "+refs/heads/main:refs/remotes/origin/main", ) commit("A") diff --git a/test/submit/multi.py.test b/test/submit/multi.py.test index 2152d1e..1cce0cc 100644 --- a/test/submit/multi.py.test +++ b/test/submit/multi.py.test @@ -7,7 +7,7 @@ A, B = gh_submit("Initial 1 and 2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -25,7 +25,7 @@ if is_direct(): | Initial 1 and 2 * 6eac261 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 and 2 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) @@ -62,7 +62,7 @@ else: | | Initial 1 and 2 | * 954b129 (gh/ezyang/2/base) |/ Initial 1 and 2 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/no_clobber.py.test b/test/submit/no_clobber.py.test index c68ad71..735751d 100644 --- a/test/submit/no_clobber.py.test +++ b/test/submit/no_clobber.py.test @@ -23,7 +23,7 @@ Directly updated message body""", if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) Stack: * **#500 Commit 1** @@ -36,7 +36,7 @@ if is_direct(): * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -58,7 +58,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -72,7 +72,7 @@ tick() if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) Stack: * __->__ #500 @@ -88,7 +88,7 @@ if is_direct(): | Update 1 * 6c1d876 | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -113,7 +113,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/no_clobber_carriage_returns.py.test b/test/submit/no_clobber_carriage_returns.py.test index cda53f2..824c562 100644 --- a/test/submit/no_clobber_carriage_returns.py.test +++ b/test/submit/no_clobber_carriage_returns.py.test @@ -14,7 +14,7 @@ tick() if is_direct(): assert_github_state( """\ - [O] #500 Commit 1 (gh/ezyang/1/head -> master) + [O] #500 Commit 1 (gh/ezyang/1/head -> main) Original message @@ -24,7 +24,7 @@ if is_direct(): * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -46,7 +46,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -72,7 +72,7 @@ tick() if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) Stack: * #501 @@ -94,7 +94,7 @@ if is_direct(): | Initial 2 * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -131,7 +131,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/non_standard_base.py.test b/test/submit/non_standard_base.py.test index f78427b..a0f5820 100644 --- a/test/submit/non_standard_base.py.test +++ b/test/submit/non_standard_base.py.test @@ -3,12 +3,12 @@ from ghstack.test_prelude import * init_test() # make release branch -git("branch", "release", "master") +git("branch", "release", "main") # diverge release and regular branch -git("checkout", "master") +git("checkout", "main") commit("M") -git("push", "origin", "master") +git("push", "origin", "main") git("checkout", "release") commit("R") diff --git a/test/submit/preserve_downstream_closed.py.test b/test/submit/preserve_downstream_closed.py.test index 1c4d514..b96c6f0 100644 --- a/test/submit/preserve_downstream_closed.py.test +++ b/test/submit/preserve_downstream_closed.py.test @@ -60,7 +60,7 @@ else: | | Initial | * 8e6f9ba (gh/ezyang/2/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_middle.py.test b/test/submit/preserve_downstream_middle.py.test index 8bb20e6..173ade9 100644 --- a/test/submit/preserve_downstream_middle.py.test +++ b/test/submit/preserve_downstream_middle.py.test @@ -73,7 +73,7 @@ else: | | Initial | * 8fe2454 (gh/ezyang/3/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_multiple.py.test b/test/submit/preserve_downstream_multiple.py.test index 0851812..6472fb2 100644 --- a/test/submit/preserve_downstream_multiple.py.test +++ b/test/submit/preserve_downstream_multiple.py.test @@ -72,7 +72,7 @@ if not is_direct(): | | Initial | * 8fe2454 (gh/ezyang/3/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_new_on_top.py.test b/test/submit/preserve_downstream_new_on_top.py.test index cf0e9bf..63e4327 100644 --- a/test/submit/preserve_downstream_new_on_top.py.test +++ b/test/submit/preserve_downstream_new_on_top.py.test @@ -20,7 +20,7 @@ gh_submit("Add C", revs=["HEAD~", "HEAD"], stack=False) if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -57,7 +57,7 @@ if is_direct(): | Initial * a6cb153 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -131,7 +131,7 @@ else: | | Initial | * 8fe2454 (gh/ezyang/3/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_prs.py.test b/test/submit/preserve_downstream_prs.py.test index eee0cd4..f1f2693 100644 --- a/test/submit/preserve_downstream_prs.py.test +++ b/test/submit/preserve_downstream_prs.py.test @@ -54,7 +54,7 @@ else: | | Initial | * 8e6f9ba (gh/ezyang/2/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/preserve_downstream_rearrange.py.test b/test/submit/preserve_downstream_rearrange.py.test index b8d0996..791622f 100644 --- a/test/submit/preserve_downstream_rearrange.py.test +++ b/test/submit/preserve_downstream_rearrange.py.test @@ -79,7 +79,7 @@ else: | | Initial | * 8fe2454 (gh/ezyang/3/base) |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/rebase.py.test b/test/submit/rebase.py.test index 3d040d3..1bd001d 100644 --- a/test/submit/rebase.py.test +++ b/test/submit/rebase.py.test @@ -7,19 +7,19 @@ commit("A") commit("B") A2, B2 = gh_submit("Initial 2") -git("checkout", "master") +git("checkout", "main") commit("M") -git("push", "origin", "master") +git("push", "origin", "main") git("checkout", "feature") -git("rebase", "origin/master") +git("rebase", "origin/main") gh_submit("Rebase") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -39,7 +39,7 @@ if is_direct(): |\\ Rebase | * b0558d8 (gh/ezyang/1/next, gh/ezyang/1/head) | |\\ Rebase - | | * 7ceeaa9 (HEAD -> master) + | | * 7ceeaa9 (HEAD -> main) | | | Commit M * | | 0243aa2 |/ / Initial 2 @@ -90,7 +90,7 @@ else: | | | |\\ Rebase (base update) | | |_|/ | |/| | - | * | | 7ceeaa9 (HEAD -> master) + | * | | 7ceeaa9 (HEAD -> main) |/ / / Commit M | * / ca4399f | |/ Initial 2 diff --git a/test/submit/remove_bottom_commit.py.test b/test/submit/remove_bottom_commit.py.test index 331d3e8..fc13471 100644 --- a/test/submit/remove_bottom_commit.py.test +++ b/test/submit/remove_bottom_commit.py.test @@ -10,20 +10,20 @@ commit("A") commit("B") A, B = gh_submit("Initial 2") -git("checkout", "master") +git("checkout", "main") cherry_pick(B) (B2,) = gh_submit("Cherry pick") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A * 2193fd2 Initial 2 - [O] #501 Commit B (gh/ezyang/2/head -> master) + [O] #501 Commit B (gh/ezyang/2/head -> main) This is commit B @@ -39,7 +39,7 @@ if is_direct(): | Initial 2 * 2193fd2 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 2 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -81,7 +81,7 @@ else: | | Initial 2 | * f081adc (gh/ezyang/1/base) |/ Initial 2 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/reorder.py.test b/test/submit/reorder.py.test index c3b2a8b..7f8f123 100644 --- a/test/submit/reorder.py.test +++ b/test/submit/reorder.py.test @@ -20,7 +20,7 @@ if is_direct(): * f61b335 Reorder - [O] #501 Commit B (gh/ezyang/2/head -> master) + [O] #501 Commit B (gh/ezyang/2/head -> main) This is commit B @@ -38,7 +38,7 @@ if is_direct(): |/ Initial * 92a6dc7 | Initial - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -85,7 +85,7 @@ else: | |/ Initial | * 8e6f9ba |/ Initial (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/reviewer_and_label.py.test b/test/submit/reviewer_and_label.py.test index 57fe7f4..e49374a 100644 --- a/test/submit/reviewer_and_label.py.test +++ b/test/submit/reviewer_and_label.py.test @@ -14,7 +14,7 @@ assert_eq(get_pr_labels(500), ["bug", "enhancement"]) if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -24,7 +24,7 @@ if is_direct(): * 9cb2ede (gh/ezyang/1/next, gh/ezyang/1/head) | Initial commit - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -46,7 +46,7 @@ else: | Initial commit * 5956f18 (gh/ezyang/1/base) | Initial commit (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/simple.py.test b/test/submit/simple.py.test index 636358b..83711b1 100644 --- a/test/submit/simple.py.test +++ b/test/submit/simple.py.test @@ -13,7 +13,7 @@ gh_submit("Initial 2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -31,7 +31,7 @@ if is_direct(): | Initial 2 * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -68,7 +68,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """, ) diff --git a/test/submit/unrelated_malformed_gh_branch_ok.py.test b/test/submit/unrelated_malformed_gh_branch_ok.py.test index 165a12c..0209a48 100644 --- a/test/submit/unrelated_malformed_gh_branch_ok.py.test +++ b/test/submit/unrelated_malformed_gh_branch_ok.py.test @@ -8,7 +8,7 @@ git("checkout", "-b", "gh/ezyang/malform") git("push", "origin", "gh/ezyang/malform") git("checkout", "-b", "gh/ezyang/non_int/head") git("push", "origin", "gh/ezyang/non_int/head") -git("checkout", "master") +git("checkout", "main") commit("A") (A,) = gh_submit("Initial 1") @@ -21,7 +21,7 @@ gh_submit("Initial 2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -39,7 +39,7 @@ if is_direct(): | Initial 2 * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master, gh/ezyang/non_int/head, gh/ezyang/malform) + * dc8bfe4 (HEAD -> main, gh/ezyang/non_int/head, gh/ezyang/malform) Initial commit """ ) @@ -76,7 +76,7 @@ else: | | Initial 1 | * 5a32949 (gh/ezyang/1/base) |/ Initial 1 (base update) - * dc8bfe4 (HEAD -> master, gh/ezyang/non_int/head, gh/ezyang/malform) + * dc8bfe4 (HEAD -> main, gh/ezyang/non_int/head, gh/ezyang/malform) Initial commit """, ) diff --git a/test/submit/update_fields.py.test b/test/submit/update_fields.py.test index ea7ed55..b10de45 100644 --- a/test/submit/update_fields.py.test +++ b/test/submit/update_fields.py.test @@ -19,7 +19,7 @@ get_github().patch( if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) Directly updated message body @@ -29,7 +29,7 @@ if is_direct(): * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -48,7 +48,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -58,7 +58,7 @@ gh_submit("Update 1", update_fields=True) if is_direct(): assert_github_state( """\ - [O] #500 Commit 1 (gh/ezyang/1/head -> master) + [O] #500 Commit 1 (gh/ezyang/1/head -> main) Original message @@ -68,7 +68,7 @@ if is_direct(): * 6c1d876 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -90,7 +90,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/update_fields_preserve_differential_revision.py.test b/test/submit/update_fields_preserve_differential_revision.py.test index a06aa89..321bb72 100644 --- a/test/submit/update_fields_preserve_differential_revision.py.test +++ b/test/submit/update_fields_preserve_differential_revision.py.test @@ -19,7 +19,7 @@ get_github().patch( if is_direct(): assert_github_state( """\ - [O] #500 Directly updated title (gh/ezyang/1/head -> master) + [O] #500 Directly updated title (gh/ezyang/1/head -> main) @@ -34,7 +34,7 @@ if is_direct(): * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -58,7 +58,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -68,7 +68,7 @@ gh_submit("Update 1", update_fields=True) if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -80,7 +80,7 @@ if is_direct(): * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -104,7 +104,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/submit/update_fields_preserves_commit_message.py.test b/test/submit/update_fields_preserves_commit_message.py.test index 84d0949..1300639 100644 --- a/test/submit/update_fields_preserves_commit_message.py.test +++ b/test/submit/update_fields_preserves_commit_message.py.test @@ -13,7 +13,7 @@ git("commit", "--amend", "-m", "Amended " + A.commit_msg) if is_direct(): assert_github_state( """\ - [O] #500 Amended Commit A (gh/ezyang/1/head -> master) + [O] #500 Amended Commit A (gh/ezyang/1/head -> main) This is commit A @@ -23,7 +23,7 @@ if is_direct(): * 8c7d059 (gh/ezyang/1/next, gh/ezyang/1/head) | Initial 1 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -45,7 +45,7 @@ else: | Initial 1 * 5a32949 (gh/ezyang/1/base) | Initial 1 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) diff --git a/test/unlink/basic.py.test b/test/unlink/basic.py.test index 2484051..895273a 100644 --- a/test/unlink/basic.py.test +++ b/test/unlink/basic.py.test @@ -14,7 +14,7 @@ gh_submit("Initial 2") if is_direct(): assert_github_state( """\ - [O] #500 Commit A (gh/ezyang/1/head -> master) + [O] #500 Commit A (gh/ezyang/1/head -> main) This is commit A @@ -26,7 +26,7 @@ if is_direct(): * 30b9f2a Initial 1 - [O] #502 Commit A (gh/ezyang/3/head -> master) + [O] #502 Commit A (gh/ezyang/3/head -> main) This is commit A @@ -54,7 +54,7 @@ if is_direct(): | | Initial 2 | * 2193fd2 (gh/ezyang/3/next, gh/ezyang/3/head) |/ Initial 2 - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) @@ -119,7 +119,7 @@ else: | | Initial 2 | * c05297f (gh/ezyang/4/base) |/ Initial 2 (base update) - * dc8bfe4 (HEAD -> master) + * dc8bfe4 (HEAD -> main) Initial commit """ ) From 95e5abfc673350b55b16ebd42c2936e0c68c8c18 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 12:58:20 -0400 Subject: [PATCH 10/18] Update [ghstack-poisoned] --- src/ghstack/github_fake.py | 49 +++++++------------------------------- 1 file changed, 9 insertions(+), 40 deletions(-) diff --git a/src/ghstack/github_fake.py b/src/ghstack/github_fake.py index 4df0184..3072c47 100644 --- a/src/ghstack/github_fake.py +++ b/src/ghstack/github_fake.py @@ -424,7 +424,7 @@ async def _update_pull_async( if "body" in input and input["body"] is not None: pr.body = input["body"] - def _create_issue_comment( + async def _create_issue_comment_async( self, owner: str, name: str, comment_id: int, input: CreateIssueCommentInput ) -> CreateIssueCommentPayload: state = self.state @@ -444,7 +444,7 @@ def _create_issue_comment( "id": comment_id, } - def _update_issue_comment( + async def _update_issue_comment_async( self, owner: str, name: str, comment_id: int, input: UpdateIssueCommentInput ) -> None: state = self.state @@ -467,7 +467,7 @@ async def _set_default_branch_async( async def arest(self, method: str, path: str, **kwargs: Any) -> Any: return await self._arest_impl(method, path, **kwargs) - def _rest_impl(self, method: str, path: str, **kwargs: Any) -> Any: + async def _arest_impl(self, method: str, path: str, **kwargs: Any) -> Any: if method == "get": m = re.match(r"^repos/([^/]+)/([^/]+)/branches/([^/]+)/protection", path) if m: @@ -499,8 +499,12 @@ def _rest_impl(self, method: str, path: str, **kwargs: Any) -> Any: } elif method == "post": + if m := re.match(r"^repos/([^/]+)/([^/]+)/pulls$", path): + return await self._create_pull_async( + m.group(1), m.group(2), cast(CreatePullRequestInput, kwargs) + ) if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/([^/]+)/comments", path): - return self._create_issue_comment( + return await self._create_issue_comment_async( m.group(1), m.group(2), GitHubNumber(int(m.group(3))), @@ -524,41 +528,6 @@ def _rest_impl(self, method: str, path: str, **kwargs: Any) -> Any: labels = kwargs.get("labels", []) pr.labels.extend(labels) return {} - elif method == "patch": - if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/comments/([^/]+)$", path): - return self._update_issue_comment( - m.group(1), - m.group(2), - int(m.group(3)), - cast(UpdateIssueCommentInput, kwargs), - ) - raise NotImplementedError( - "FakeGitHubEndpoint REST {} {} not implemented".format(method.upper(), path) - ) - - async def _arest_impl(self, method: str, path: str, **kwargs: Any) -> Any: - if method == "get": - return self._rest_impl(method, path, **kwargs) - - elif method == "post": - if m := re.match(r"^repos/([^/]+)/([^/]+)/pulls$", path): - return await self._create_pull_async( - m.group(1), m.group(2), cast(CreatePullRequestInput, kwargs) - ) - if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/([^/]+)/comments", path): - return self._create_issue_comment( - m.group(1), - m.group(2), - GitHubNumber(int(m.group(3))), - cast(CreateIssueCommentInput, kwargs), - ) - if m := re.match( - r"^repos/([^/]+)/([^/]+)/pulls/([^/]+)/requested_reviewers", path - ): - return self._rest_impl(method, path, **kwargs) - if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/([^/]+)/labels", path): - return self._rest_impl(method, path, **kwargs) - elif method == "patch": if m := re.match(r"^repos/([^/]+)/([^/]+)(?:/pulls/([^/]+))?$", path): owner, name, number = m.groups() @@ -574,7 +543,7 @@ async def _arest_impl(self, method: str, path: str, **kwargs: Any) -> Any: owner, name, cast(SetDefaultBranchInput, kwargs) ) if m := re.match(r"^repos/([^/]+)/([^/]+)/issues/comments/([^/]+)$", path): - return self._update_issue_comment( + return await self._update_issue_comment_async( m.group(1), m.group(2), int(m.group(3)), From c29e973a5efffe8d2f67de61d5d1154dfbd28c30 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 17:25:23 -0400 Subject: [PATCH 11/18] Update [ghstack-poisoned] --- conftest.py | 10 +- src/ghstack/action.py | 8 +- src/ghstack/async_script.py | 57 +++++ src/ghstack/checkout.py | 16 +- src/ghstack/cherry_pick.py | 18 +- src/ghstack/cli.py | 166 +++++++------ src/ghstack/forensics.py | 4 +- src/ghstack/github.py | 52 +--- src/ghstack/github_fake.py | 26 +- src/ghstack/github_real.py | 95 ++++---- src/ghstack/github_utils.py | 39 +-- src/ghstack/gpg_sign.py | 4 +- src/ghstack/land.py | 53 ++-- src/ghstack/log.py | 29 +-- src/ghstack/shell.py | 101 ++------ src/ghstack/status.py | 4 +- src/ghstack/submit.py | 226 +++++++++--------- src/ghstack/sync.py | 23 +- src/ghstack/test_prelude.py | 116 +++++---- src/ghstack/unlink.py | 19 +- test/checkout/basic.py.test | 16 +- test/checkout/same_base_allows.py.test | 16 +- test/checkout/same_base_rejects.py.test | 24 +- test/cherry_pick/basic.py.test | 18 +- test/cherry_pick/conflict.py.test | 20 +- test/cherry_pick/stack.py.test | 20 +- test/cherry_pick/stack_manual.py.test | 22 +- .../get_repo_name_with_owner.py.test | 30 +-- test/land/default_branch_change.py.test | 36 +-- test/land/early_mod.py.test | 20 +- test/land/ff.py.test | 10 +- test/land/ff_stack.py.test | 12 +- test/land/ff_stack_two_phase.py.test | 14 +- test/land/invalid_resubmit.py.test | 26 +- test/land/non_ff.py.test | 18 +- test/land/non_ff_stack_two_phase.py.test | 20 +- test/land/reuse_branch_refuse_land.py.test | 14 +- test/land/update_after_land.py.test | 30 +-- test/log/basic.py.test | 20 +- test/log/explicit_pr.py.test | 10 +- test/log/pending_diff.py.test | 14 +- test/log/stack.py.test | 20 +- test/submit/amend.py.test | 14 +- test/submit/amend_all.py.test | 24 +- test/submit/amend_bottom.py.test | 24 +- test/submit/amend_message_only.py.test | 14 +- test/submit/amend_out_of_date.py.test | 16 +- test/submit/amend_top.py.test | 18 +- test/submit/bullet_divider.py.test | 12 +- test/submit/cherry_pick.py.test | 24 +- test/submit/cli_reviewer_and_label.py.test | 14 +- test/submit/commit_amended_to_empty.py.test | 16 +- ...ot_revert_local_commit_msg_on_skip.py.test | 18 +- test/submit/empty_commit.py.test | 8 +- test/submit/fail_same_source_id.py.test | 12 +- test/submit/minimal_fetch.py.test | 12 +- test/submit/multi.py.test | 10 +- test/submit/no_clobber.py.test | 24 +- .../no_clobber_carriage_returns.py.test | 24 +- test/submit/non_standard_base.py.test | 24 +- test/submit/prefix_only_no_stack.py.test | 16 +- test/submit/prefix_only_stack.py.test | 20 +- test/submit/preserve_authorship.py.test | 10 +- .../submit/preserve_downstream_closed.py.test | 16 +- .../submit/preserve_downstream_middle.py.test | 20 +- .../preserve_downstream_multiple.py.test | 22 +- .../preserve_downstream_new_on_top.py.test | 22 +- test/submit/preserve_downstream_prs.py.test | 18 +- .../preserve_downstream_rearrange.py.test | 20 +- test/submit/range_only_stack.py.test | 24 +- test/submit/rebase.py.test | 26 +- test/submit/reject_head_stack.py.test | 18 +- test/submit/remove_bottom_commit.py.test | 20 +- test/submit/reorder.py.test | 20 +- test/submit/reviewer_and_label.py.test | 10 +- test/submit/short.py.test | 6 +- test/submit/simple.py.test | 16 +- test/submit/strip_mentions.py.test | 18 +- test/submit/suffix_only_no_stack.py.test | 16 +- test/submit/throttle.py.test | 6 +- .../unrelated_malformed_gh_branch_ok.py.test | 26 +- test/submit/update_fields.py.test | 20 +- ...lds_preserve_differential_revision.py.test | 18 +- ...te_fields_preserves_commit_message.py.test | 16 +- test/sync/basic.py.test | 22 +- test/unlink/basic.py.test | 16 +- test_shell.py | 40 ++-- 87 files changed, 1165 insertions(+), 1141 deletions(-) create mode 100644 src/ghstack/async_script.py diff --git a/conftest.py b/conftest.py index 405880f..e9cae79 100644 --- a/conftest.py +++ b/conftest.py @@ -1,10 +1,11 @@ # mypy: ignore-errors +import asyncio import pathlib -import runpy import expecttest +import ghstack.async_script import ghstack.test_prelude import pytest @@ -33,9 +34,12 @@ def __init__(self, *, direct, **kwargs): self.direct = direct def runtest(self): - with ghstack.test_prelude.scoped_test(direct=self.direct): + asyncio.run(self.aruntest()) + + async def aruntest(self): + async with ghstack.test_prelude.scoped_test(direct=self.direct): expecttest.EDIT_HISTORY.reload_file(self.fspath) - runpy.run_path(self.fspath) + await ghstack.async_script.run_path(str(self.fspath)) def repr_failure(self, excinfo): excinfo.traceback = excinfo.traceback.cut(path=self.fspath) diff --git a/src/ghstack/action.py b/src/ghstack/action.py index ac6e1b6..580acf4 100644 --- a/src/ghstack/action.py +++ b/src/ghstack/action.py @@ -8,15 +8,15 @@ import ghstack.shell -def main( +async def main( pull_request: str, github: ghstack.github.GitHubEndpoint, sh: Optional[ghstack.shell.Shell] = None, close: bool = False, ) -> None: - params = ghstack.github_utils.parse_pull_request(pull_request) - pr_result = github.graphql( + params = await ghstack.github_utils.parse_pull_request(pull_request) + pr_result = await github.graphql( """ query ($owner: String!, $name: String!, $number: Int!) { repository(name: $name, owner: $owner) { @@ -32,7 +32,7 @@ def main( if close: logging.info("Closing {owner}/{name}#{number}".format(**params)) - github.graphql( + await github.graphql( """ mutation ($input: ClosePullRequestInput!) { closePullRequest(input: $input) { diff --git a/src/ghstack/async_script.py b/src/ghstack/async_script.py new file mode 100644 index 0000000..fdd80bf --- /dev/null +++ b/src/ghstack/async_script.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 + +import argparse +import ast +import asyncio +import inspect +import sys +from pathlib import Path +from typing import Any, Dict, Optional, Sequence + + +async def run_path( + path: str, + *, + argv: Optional[Sequence[str]] = None, + globals_: Optional[Dict[str, Any]] = None, +) -> Dict[str, Any]: + script_path = str(Path(path)) + script_argv = [script_path, *(argv or ())] + old_argv = sys.argv + sys.argv = script_argv + try: + source = Path(script_path).read_text() + namespace: Dict[str, Any] = { + "__file__": script_path, + "__name__": "__main__", + "__package__": None, + "__builtins__": __builtins__, + } + if globals_ is not None: + namespace.update(globals_) + code = compile( + source, + script_path, + "exec", + flags=ast.PyCF_ALLOW_TOP_LEVEL_AWAIT, + ) + result = eval(code, namespace) + if inspect.isawaitable(result): + await result + return namespace + finally: + sys.argv = old_argv + + +def main(argv: Optional[Sequence[str]] = None) -> None: + parser = argparse.ArgumentParser( + description="Run a Python script with top-level await support." + ) + parser.add_argument("script") + parser.add_argument("args", nargs=argparse.REMAINDER) + ns = parser.parse_args(argv) + asyncio.run(run_path(ns.script, argv=ns.args)) + + +if __name__ == "__main__": + main() diff --git a/src/ghstack/checkout.py b/src/ghstack/checkout.py index 2a7f1f8..13230f7 100644 --- a/src/ghstack/checkout.py +++ b/src/ghstack/checkout.py @@ -8,7 +8,7 @@ import ghstack.shell -def main( +async def main( pull_request: str, github: ghstack.github.GitHubEndpoint, sh: ghstack.shell.Shell, @@ -16,10 +16,10 @@ def main( same_base: bool = False, ) -> None: - params = ghstack.github_utils.parse_pull_request( + params = await ghstack.github_utils.parse_pull_request( pull_request, sh=sh, remote_name=remote_name ) - head_ref = github.get_head_ref(**params) + head_ref = await github.get_head_ref(**params) orig_ref = re.sub(r"/head$", "/orig", head_ref) if orig_ref == head_ref: logging.warning( @@ -31,7 +31,7 @@ def main( # If --same-base is specified, check if checkout would change the merge-base if same_base: # Get the default branch name from the repo - repo_info = ghstack.github_utils.get_github_repo_info( + repo_info = await ghstack.github_utils.get_github_repo_info( github=github, sh=sh, repo_owner=params["owner"], @@ -43,19 +43,19 @@ def main( default_branch_ref = f"{remote_name}/{default_branch}" # Get current merge-base with default branch - current_base = sh.git("merge-base", default_branch_ref, "HEAD") + current_base = await sh.agit("merge-base", default_branch_ref, "HEAD") else: current_base = None default_branch_ref = None - sh.git("fetch", "--prune", remote_name) + await sh.agit("fetch", "--prune", remote_name) # If --same-base is specified, check what the new merge-base would be if same_base: assert default_branch_ref is not None assert current_base is not None target_ref = remote_name + "/" + orig_ref - new_base = sh.git("merge-base", default_branch_ref, target_ref) + new_base = await sh.agit("merge-base", default_branch_ref, target_ref) if current_base != new_base: raise RuntimeError( @@ -63,4 +63,4 @@ def main( f"aborting due to --same-base flag" ) - sh.git("checkout", remote_name + "/" + orig_ref) + await sh.agit("checkout", remote_name + "/" + orig_ref) diff --git a/src/ghstack/cherry_pick.py b/src/ghstack/cherry_pick.py index 85e34e3..1d2b69c 100644 --- a/src/ghstack/cherry_pick.py +++ b/src/ghstack/cherry_pick.py @@ -8,7 +8,7 @@ import ghstack.shell -def main( +async def main( pull_request: str, github: ghstack.github.GitHubEndpoint, sh: ghstack.shell.Shell, @@ -17,10 +17,10 @@ def main( no_fetch: bool = False, ) -> None: - params = ghstack.github_utils.parse_pull_request( + params = await ghstack.github_utils.parse_pull_request( pull_request, sh=sh, remote_name=remote_name ) - head_ref = github.get_head_ref(**params) + head_ref = await github.get_head_ref(**params) orig_ref = re.sub(r"/head$", "/orig", head_ref) if orig_ref == head_ref: logging.warning( @@ -28,14 +28,14 @@ def main( ) if not no_fetch: - sh.git("fetch", "--prune", remote_name) + await sh.agit("fetch", "--prune", remote_name) if stack: # Cherry-pick the entire stack from merge-base to the commit remote_orig_ref = remote_name + "/" + orig_ref # Find the merge-base with the main branch - repo_info = ghstack.github_utils.get_github_repo_info( + repo_info = await ghstack.github_utils.get_github_repo_info( github=github, sh=sh, github_url=params["github_url"], @@ -44,11 +44,11 @@ def main( main_branch = f"{remote_name}/{repo_info['default_branch']}" # Get merge-base between the commit and main branch - merge_base = sh.git("merge-base", main_branch, remote_orig_ref).strip() + merge_base = (await sh.agit("merge-base", main_branch, remote_orig_ref)).strip() # Get all commits from merge-base to the target commit commit_list = ( - sh.git("rev-list", "--reverse", f"{merge_base}..{remote_orig_ref}") + (await sh.agit("rev-list", "--reverse", f"{merge_base}..{remote_orig_ref}")) .strip() .split("\n") ) @@ -58,10 +58,10 @@ def main( logging.info(f"Cherry-picking {len(commit_list)} commits from stack") for commit in commit_list: - sh.git("cherry-pick", commit) + await sh.agit("cherry-pick", commit) logging.info(f"Cherry-picked {commit}") else: # Cherry-pick just the single commit remote_orig_ref = remote_name + "/" + orig_ref - sh.git("cherry-pick", remote_orig_ref) + await sh.agit("cherry-pick", remote_orig_ref) logging.info(f"Cherry-picked {orig_ref}") diff --git a/src/ghstack/cli.py b/src/ghstack/cli.py index 1d95ec2..24f77ca 100644 --- a/src/ghstack/cli.py +++ b/src/ghstack/cli.py @@ -1,7 +1,6 @@ import asyncio import contextlib -import sys -from typing import Generator, List, Optional, Tuple +from typing import Any, Coroutine, Generator, List, Optional, Tuple import click @@ -30,6 +29,10 @@ ] +def run_async(coro: Coroutine[Any, Any, object]) -> object: + return asyncio.run(coro) + + @contextlib.contextmanager def cli_context( *, @@ -136,12 +139,6 @@ def main( """ Submit stacks of diffs to Github """ - if sys.version_info >= (3, 14): - # Create new event loop as asyncio.get_event_loop() throws runtime error in 3.14 - import asyncio as _asyncio - - _asyncio.set_event_loop(_asyncio.new_event_loop()) - EXIT_STACK.enter_context(ghstack.logs.manager(debug=debug)) if not ctx.invoked_subcommand: @@ -178,11 +175,13 @@ def action(close: bool, pull_request: str) -> None: Perform actions on a PR """ with cli_context() as (shell, _, github): - ghstack.action.main( - pull_request=pull_request, - github=github, - sh=shell, - close=close, + run_async( + ghstack.action.main( + pull_request=pull_request, + github=github, + sh=shell, + close=close, + ) ) @@ -198,12 +197,14 @@ def checkout(same_base: bool, pull_request: str) -> None: Checkout a PR """ with cli_context(request_github_token=False) as (shell, config, github): - ghstack.checkout.main( - pull_request=pull_request, - github=github, - sh=shell, - remote_name=config.remote_name, - same_base=same_base, + run_async( + ghstack.checkout.main( + pull_request=pull_request, + github=github, + sh=shell, + remote_name=config.remote_name, + same_base=same_base, + ) ) @@ -225,13 +226,15 @@ def cherry_pick(stack: bool, no_fetch: bool, pull_request: str) -> None: Cherry-pick a PR """ with cli_context(request_github_token=False) as (shell, config, github): - ghstack.cherry_pick.main( - pull_request=pull_request, - github=github, - sh=shell, - remote_name=config.remote_name, - stack=stack, - no_fetch=no_fetch, + run_async( + ghstack.cherry_pick.main( + pull_request=pull_request, + github=github, + sh=shell, + remote_name=config.remote_name, + stack=stack, + no_fetch=no_fetch, + ) ) @@ -243,13 +246,15 @@ def land(force: bool, pull_request: str) -> None: Land a PR stack """ with cli_context() as (shell, config, github): - ghstack.land.main( - pull_request=pull_request, - github=github, - sh=shell, - github_url=config.github_url, - remote_name=config.remote_name, - force=force, + run_async( + ghstack.land.main( + pull_request=pull_request, + github=github, + sh=shell, + github_url=config.github_url, + remote_name=config.remote_name, + force=force, + ) ) @@ -272,13 +277,15 @@ def log(pull_request: Optional[str], git_log_args: Tuple[str, ...]) -> None: Extra arguments are forwarded to git log (e.g. -p). """ with cli_context(request_github_token=False) as (shell, config, github): - ghstack.log.main( - github=github, - sh=shell, - remote_name=config.remote_name, - github_url=config.github_url, - args=list(git_log_args), - pull_request=pull_request, + run_async( + ghstack.log.main( + github=github, + sh=shell, + remote_name=config.remote_name, + github_url=config.github_url, + args=list(git_log_args), + pull_request=pull_request, + ) ) @@ -304,14 +311,13 @@ def status(pull_request: str) -> None: circle_token=config.circle_token ) - fut = ghstack.status.main( - pull_request=pull_request, - github=github, - circleci=circleci, + run_async( + ghstack.status.main( + pull_request=pull_request, + github=github, + circleci=circleci, + ) ) - loop = asyncio.get_event_loop() - loop.run_until_complete(fut) - loop.close() @main.command("submit") @@ -413,25 +419,27 @@ def submit( Submit or update a PR stack """ with cli_context() as (shell, config, github): - ghstack.submit.main( - msg=message, - username=config.github_username, - sh=shell, - github=github, - update_fields=update_fields, - short=short, - force=force, - no_skip=no_skip, - draft=draft, - github_url=config.github_url, - remote_name=config.remote_name, - base_opt=base, - revs=revs, - stack=stack, - direct_opt=direct_opt, - reviewer=reviewer if reviewer is not None else config.reviewer, - label=label if label is not None else config.label, - no_fetch=no_fetch, + run_async( + ghstack.submit.main( + msg=message, + username=config.github_username, + sh=shell, + github=github, + update_fields=update_fields, + short=short, + force=force, + no_skip=no_skip, + draft=draft, + github_url=config.github_url, + remote_name=config.remote_name, + base_opt=base, + revs=revs, + stack=stack, + direct_opt=direct_opt, + reviewer=reviewer if reviewer is not None else config.reviewer, + label=label if label is not None else config.label, + no_fetch=no_fetch, + ) ) @@ -441,11 +449,13 @@ def sync() -> None: Sync PR descriptions from GitHub back to local commit messages """ with cli_context() as (shell, config, github): - ghstack.sync.main( - github=github, - sh=shell, - github_url=config.github_url, - remote_name=config.remote_name, + run_async( + ghstack.sync.main( + github=github, + sh=shell, + github_url=config.github_url, + remote_name=config.remote_name, + ) ) @@ -456,10 +466,12 @@ def unlink(commits: List[str]) -> None: Unlink commits from PRs """ with cli_context() as (shell, config, github): - ghstack.unlink.main( - commits=commits, - github=github, - sh=shell, - github_url=config.github_url, - remote_name=config.remote_name, + run_async( + ghstack.unlink.main( + commits=commits, + github=github, + sh=shell, + github_url=config.github_url, + remote_name=config.remote_name, + ) ) diff --git a/src/ghstack/forensics.py b/src/ghstack/forensics.py index 479c7cd..e99cc36 100644 --- a/src/ghstack/forensics.py +++ b/src/ghstack/forensics.py @@ -46,10 +46,10 @@ async def main( # to be any indication that a halt was called. So we'll # have to rely on the (OS X jobs, take note!) - params = ghstack.github_utils.parse_pull_request(pull_request) + params = await ghstack.github_utils.parse_pull_request(pull_request) # TODO: stop hard-coding number of commits - r = github.graphql( + r = await github.graphql( """ query ($name: String!, $owner: String!, $number: Int!) { repository(name: $name, owner: $owner) { diff --git a/src/ghstack/github.py b/src/ghstack/github.py index f03dd27..d1c6258 100644 --- a/src/ghstack/github.py +++ b/src/ghstack/github.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import asyncio from abc import ABCMeta, abstractmethod from typing import Any, Sequence @@ -13,7 +12,7 @@ class NotFoundError(RuntimeError): class GitHubEndpoint(metaclass=ABCMeta): @abstractmethod - def graphql(self, query: str, **kwargs: Any) -> Any: + async def graphql(self, query: str, **kwargs: Any) -> Any: """ Args: query: string GraphQL query to execute @@ -23,13 +22,13 @@ def graphql(self, query: str, **kwargs: Any) -> Any: """ pass - def get_head_ref(self, **params: Any) -> str: + async def get_head_ref(self, **params: Any) -> str: """ Fetch the headRefName associated with a PR. Defaults to a GraphQL query but if we're hitting a real GitHub endpoint we'll do a regular HTTP request to avoid rate limit. """ - pr_result = self.graphql( + pr_result = await self.graphql( """ query ($owner: String!, $name: String!, $number: Int!) { repository(name: $name, owner: $owner) { @@ -59,51 +58,6 @@ def push_hook(self, refName: Sequence[str]) -> None: def notify_merged(self, pr_resolved: ghstack.diff.PullRequestResolved) -> None: pass - def get(self, path: str, **kwargs: Any) -> Any: - """ - Send a GET request to endpoint 'path'. - - Returns: parsed JSON response - """ - return self._run_async(self.aget(path, **kwargs)) - - def post(self, path: str, **kwargs: Any) -> Any: - """ - Send a POST request to endpoint 'path'. - - Returns: parsed JSON response - """ - return self._run_async(self.apost(path, **kwargs)) - - def patch(self, path: str, **kwargs: Any) -> Any: - """ - Send a PATCH request to endpoint 'path'. - - Returns: parsed JSON response - """ - return self._run_async(self.apatch(path, **kwargs)) - - def rest(self, method: str, path: str, **kwargs: Any) -> Any: - """ - Send a 'method' request to endpoint 'path'. - - Args: - method: 'GET', 'POST', etc. - path: relative URL path to access on endpoint - **kwargs: dictionary of JSON payload to send - - Returns: parsed JSON response - """ - return self._run_async(self.arest(method, path, **kwargs)) - - @staticmethod - def _run_async(coro: Any) -> Any: - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() - async def aget(self, path: str, **kwargs: Any) -> Any: return await self.arest("get", path, **kwargs) diff --git a/src/ghstack/github_fake.py b/src/ghstack/github_fake.py index 3072c47..4871312 100644 --- a/src/ghstack/github_fake.py +++ b/src/ghstack/github_fake.py @@ -161,6 +161,8 @@ def __init__(self, upstream_sh: Optional[ghstack.shell.Shell]) -> None: self._next_issue_comment_full_database_id[GraphQLId("1000")] = 1500 self.upstream_sh = upstream_sh + + async def initialize(self) -> None: if self.upstream_sh is not None: # Setup upstream Git repository representing the # pytorch/pytorch repository in the directory specified @@ -168,16 +170,19 @@ def __init__(self, upstream_sh: Optional[ghstack.shell.Shell]) -> None: # operations depend on repository state (e.g., what # the headRef is at the time a PR is created), so # we need this information - self.upstream_sh.git("init", "--bare", "-b", "main") - tree = self.upstream_sh.git("write-tree") - commit = self.upstream_sh.git("commit-tree", tree, input="Initial commit") - self.upstream_sh.git("branch", "-f", "main", commit) + await self.upstream_sh.agit("init", "--bare", "-b", "main") + tree = await self.upstream_sh.agit("write-tree") + commit = await self.upstream_sh.agit( + "commit-tree", tree, input="Initial commit" + ) + await self.upstream_sh.agit("branch", "-f", "main", commit) # We only update this when a PATCH changes the default # branch; hopefully that's fine? In any case, it should # work for now since currently we only ever access the name # of the default branch rather than other parts of its ref. - repo.defaultBranchRef = repo._make_ref(self, "main") + repo = self.repository("pytorch", "pytorch") + repo.defaultBranchRef = await repo._make_ref_async(self, "main") @dataclass @@ -216,13 +221,6 @@ def pullRequests(self, info: GraphQLResolveInfo) -> "PullRequestConnection": ) ) - # TODO: This should take which repository the ref is in - # This only works if you have upstream_sh - def _make_ref(self, state: GitHubState, refName: str) -> "Ref": - return ghstack.github.GitHubEndpoint._run_async( - self._make_ref_async(state, refName) - ) - async def _make_ref_async(self, state: GitHubState, refName: str) -> "Ref": # TODO: Probably should preserve object identity here when # you call this with refName/oid that are the same @@ -343,8 +341,8 @@ class FakeGitHubEndpoint(ghstack.github.GitHubEndpoint): def __init__(self, upstream_sh: Optional[ghstack.shell.Shell] = None) -> None: self.state = GitHubState(upstream_sh) - def graphql(self, query: str, **kwargs: Any) -> Any: - r = graphql.graphql_sync( + async def graphql(self, query: str, **kwargs: Any) -> Any: + r = await graphql.graphql( schema=GITHUB_SCHEMA, source=query, root_value=self.state.root, diff --git a/src/ghstack/github_real.py b/src/ghstack/github_real.py index ec0c9f5..b0b10f7 100644 --- a/src/ghstack/github_real.py +++ b/src/ghstack/github_real.py @@ -10,7 +10,6 @@ from typing import Any, Dict, Optional, Sequence, Tuple, Union import aiohttp -import requests import ghstack.github @@ -55,7 +54,7 @@ def rest_endpoint(self) -> str: # The certificate bundle to be used to verify the connection. # Passed to requests as 'verify'. - verify: Optional[str] + verify: Optional[Union[str, bool]] # Client side certificate to use when connecitng. # Passed to requests as 'cert'. @@ -66,7 +65,7 @@ def __init__( oauth_token: Optional[str], github_url: str, proxy: Optional[str] = None, - verify: Optional[str] = None, + verify: Optional[Union[str, bool]] = None, cert: Optional[Union[str, Tuple[str, str]]] = None, ): self.oauth_token = oauth_token @@ -79,7 +78,7 @@ def __init__( def push_hook(self, refName: Sequence[str]) -> None: pass - def graphql(self, query: str, **kwargs: Any) -> Any: + async def graphql(self, query: str, **kwargs: Any) -> Any: headers = {} if self.oauth_token: headers["Authorization"] = "bearer {}".format(self.oauth_token) @@ -92,61 +91,69 @@ def graphql(self, query: str, **kwargs: Any) -> Any: "Request GraphQL variables:\n{}".format(json.dumps(kwargs, indent=1)) ) - resp = requests.post( - self.graphql_endpoint.format(github_url=self.github_url), - json={"query": query, "variables": kwargs}, - headers=headers, - proxies=self._proxies(), - verify=self.verify, - cert=self.cert, - ) - - logging.debug("Response status: {}".format(resp.status_code)) + request_kwargs: Dict[str, Any] = { + "json": {"query": query, "variables": kwargs}, + "headers": headers, + } + if self.proxy: + request_kwargs["proxy"] = self.proxy + aiohttp_ssl = self._aiohttp_ssl() + if aiohttp_ssl is not None: + request_kwargs["ssl"] = aiohttp_ssl - try: - r = resp.json() - except ValueError: - logging.debug("Response body:\n{}".format(resp.text)) - raise - else: - pretty_json = json.dumps(r, indent=1) - logging.debug("Response JSON:\n{}".format(pretty_json)) - - # Actually, this code is dead on the GitHub GraphQL API, because - # they seem to always return 200, even in error case (as of - # 11/5/2018) - try: - resp.raise_for_status() - except requests.HTTPError: - raise RuntimeError(pretty_json) + async with aiohttp.ClientSession() as session: + async with session.post( + self.graphql_endpoint.format(github_url=self.github_url), + **request_kwargs, + ) as resp: + logging.debug("Response status: {}".format(resp.status)) + + try: + r = await resp.json() + except (aiohttp.ContentTypeError, ValueError): + logging.debug("Response body:\n{}".format(await resp.text())) + raise + else: + pretty_json = json.dumps(r, indent=1) + logging.debug("Response JSON:\n{}".format(pretty_json)) + + # Actually, this code is dead on the GitHub GraphQL API, because + # they seem to always return 200, even in error case (as of + # 11/5/2018) + if resp.status >= 400: + raise RuntimeError(pretty_json) if "errors" in r: raise RuntimeError(pretty_json) return r - def get_head_ref(self, **params: Any) -> str: + async def get_head_ref(self, **params: Any) -> str: if self.oauth_token: - return super().get_head_ref(**params) + return await super().get_head_ref(**params) else: owner = params["owner"] name = params["name"] number = params["number"] - resp = requests.get( - f"{self.www_endpoint.format(github_url=self.github_url)}/{owner}/{name}/pull/{number}", - proxies=self._proxies(), - verify=self.verify, - cert=self.cert, - ) - logging.debug("Response status: {}".format(resp.status_code)) - - r = resp.text + request_kwargs: Dict[str, Any] = {} + if self.proxy: + request_kwargs["proxy"] = self.proxy + aiohttp_ssl = self._aiohttp_ssl() + if aiohttp_ssl is not None: + request_kwargs["ssl"] = aiohttp_ssl + async with aiohttp.ClientSession() as session: + async with session.get( + f"{self.www_endpoint.format(github_url=self.github_url)}/{owner}/{name}/pull/{number}", + **request_kwargs, + ) as resp: + logging.debug("Response status: {}".format(resp.status)) + r = await resp.text() + if m := re.search(r' Dict[str, str]: if self.proxy: diff --git a/src/ghstack/github_utils.py b/src/ghstack/github_utils.py index 71c3c3e..11d77fc 100644 --- a/src/ghstack/github_utils.py +++ b/src/ghstack/github_utils.py @@ -4,7 +4,7 @@ import logging import os import re -from typing import Optional +from typing import cast, Optional from typing_extensions import TypedDict @@ -21,7 +21,7 @@ ) -def get_github_repo_name_with_owner( +async def get_github_repo_name_with_owner( *, sh: ghstack.shell.Shell, github_url: str, @@ -30,7 +30,7 @@ def get_github_repo_name_with_owner( # Grovel in remotes to figure it out # Use --push to get the push URL, which is what matters for determining # where commits will actually be pushed to - remote_url = sh.git("remote", "get-url", "--push", remote_name) + remote_url = await sh.agit("remote", "get-url", "--push", remote_name) while True: match = r"^git@{github_url}:/?([^/]+)/(.+?)(?:\.git)?$".format( github_url=github_url @@ -63,12 +63,12 @@ def get_github_repo_name_with_owner( ) -def _repo_info_cache_path(sh: ghstack.shell.Shell) -> str: - git_dir = sh.abspath(sh.git("rev-parse", "--git-dir")) +async def _repo_info_cache_path(sh: ghstack.shell.Shell) -> str: + git_dir = sh.abspath(await sh.agit("rev-parse", "--git-dir")) return os.path.join(git_dir, "ghstack-repo-info.json") -def get_github_repo_info( +async def get_github_repo_info( *, github: ghstack.github.GitHubEndpoint, sh: ghstack.shell.Shell, @@ -78,7 +78,7 @@ def get_github_repo_info( remote_name: str, ) -> GitHubRepoInfo: if repo_owner is None or repo_name is None: - name_with_owner = get_github_repo_name_with_owner( + name_with_owner = await get_github_repo_name_with_owner( sh=sh, github_url=github_url, remote_name=remote_name, @@ -86,7 +86,7 @@ def get_github_repo_info( else: name_with_owner = {"owner": repo_owner, "name": repo_name} - cache_path = _repo_info_cache_path(sh) + cache_path = await _repo_info_cache_path(sh) try: with open(cache_path) as f: cached = json.load(f) @@ -96,12 +96,14 @@ def get_github_repo_info( and cached.get("default_branch") ): logging.debug("Using cached repo info from %s", cache_path) - return cached - except (OSError, json.JSONDecodeError, KeyError): - pass + return cast(GitHubRepoInfo, cached) + except (OSError, json.JSONDecodeError, KeyError) as e: + logging.debug( + "Ignoring unreadable GitHub repo info cache %s: %s", cache_path, e + ) try: - repo = github.graphql( + repo_result = await github.graphql( """ query ($owner: String!, $name: String!) { repository(name: $name, owner: $owner) { @@ -114,7 +116,8 @@ def get_github_repo_info( }""", owner=name_with_owner["owner"], name=name_with_owner["name"], - )["data"]["repository"] + ) + repo = repo_result["data"]["repository"] except RuntimeError as e: # Check if this is a repository access error (NOT_FOUND) error_msg = str(e) @@ -148,8 +151,8 @@ def get_github_repo_info( try: with open(cache_path, "w") as f: json.dump(result, f) - except OSError: - pass + except OSError as e: + logging.debug("Failed to write GitHub repo info cache %s: %s", cache_path, e) return result @@ -178,7 +181,7 @@ def _normalize_remote_url(remote_url: str) -> str: return re.sub(r"\.git$", "", remote_url) -def parse_pull_request( +async def parse_pull_request( pull_request: str, *, sh: Optional[ghstack.shell.Shell] = None, @@ -189,10 +192,10 @@ def parse_pull_request( if not m: # We can reconstruct the URL if just a PR number is passed if sh is not None and remote_name is not None: - remote_url = sh.git("remote", "get-url", "--push", remote_name) + remote_url = await sh.agit("remote", "get-url", "--push", remote_name) # Do not pass the shell to avoid infinite loop try: - return parse_pull_request( + return await parse_pull_request( _normalize_remote_url(remote_url) + "/pull/" + pull_request ) except RuntimeError: diff --git a/src/ghstack/gpg_sign.py b/src/ghstack/gpg_sign.py index f4c01f2..74e828a 100644 --- a/src/ghstack/gpg_sign.py +++ b/src/ghstack/gpg_sign.py @@ -19,7 +19,7 @@ _should_sign = None -def gpg_args_if_necessary( +async def gpg_args_if_necessary( shell: ghstack.shell.Shell = ghstack.shell.Shell(), ) -> Union[Tuple[str], Tuple[()]]: global _should_sign @@ -29,7 +29,7 @@ def gpg_args_if_necessary( try: # Why the complicated compare # https://git-scm.com/docs/git-config#Documentation/git-config.txt-boolean - _should_sign = shell.git("config", "--get", "commit.gpgsign") in ( + _should_sign = await shell.agit("config", "--get", "commit.gpgsign") in ( "yes", "on", "true", diff --git a/src/ghstack/land.py b/src/ghstack/land.py index 47ff2fa..f3f81c0 100644 --- a/src/ghstack/land.py +++ b/src/ghstack/land.py @@ -12,10 +12,10 @@ from ghstack.types import GitCommitHash -def lookup_pr_to_orig_ref_and_closed( +async def lookup_pr_to_orig_ref_and_closed( github: ghstack.github.GitHubEndpoint, *, owner: str, name: str, number: int ) -> Tuple[str, bool]: - pr_result = github.graphql( + pr_result = await github.graphql( """ query ($owner: String!, $name: String!, $number: Int!) { repository(name: $name, owner: $owner) { @@ -42,7 +42,7 @@ def lookup_pr_to_orig_ref_and_closed( return orig_ref, closed -def main( +async def main( pull_request: str, remote_name: str, github: ghstack.github.GitHubEndpoint, @@ -57,21 +57,22 @@ def main( # Furthermore, the parent commits of PR are ignored: we always # take the canonical version of the patch from any given pr - params = ghstack.github_utils.parse_pull_request( + params = await ghstack.github_utils.parse_pull_request( pull_request, sh=sh, remote_name=remote_name ) - default_branch = ghstack.github_utils.get_github_repo_info( + repo_info = await ghstack.github_utils.get_github_repo_info( github=github, sh=sh, repo_owner=params["owner"], repo_name=params["name"], github_url=github_url, remote_name=remote_name, - )["default_branch"] + ) + default_branch = repo_info["default_branch"] needs_force = False try: - protection = github.get( + protection = await github.aget( f"repos/{params['owner']}/{params['name']}/branches/{default_branch}/protection" ) if not protection["allow_force_pushes"]["enabled"]: @@ -89,7 +90,7 @@ def main( except ghstack.github.NotFoundError: pass - orig_ref, closed = lookup_pr_to_orig_ref_and_closed( + orig_ref, closed = await lookup_pr_to_orig_ref_and_closed( github, owner=params["owner"], name=params["name"], @@ -104,27 +105,27 @@ def main( sh = ghstack.shell.Shell() # Get up-to-date - sh.git("fetch", "--prune", remote_name) + await sh.agit("fetch", "--prune", remote_name) remote_orig_ref = remote_name + "/" + orig_ref base = GitCommitHash( - sh.git("merge-base", f"{remote_name}/{default_branch}", remote_orig_ref) + await sh.agit("merge-base", f"{remote_name}/{default_branch}", remote_orig_ref) ) # compute the stack of commits in chronological order (does not # include base) stack = ghstack.git.parse_header( - sh.git("rev-list", "--reverse", "--header", "^" + base, remote_orig_ref), + await sh.agit("rev-list", "--reverse", "--header", "^" + base, remote_orig_ref), github_url=github_url, ) # Switch working copy try: - prev_ref = sh.git("symbolic-ref", "--short", "HEAD") + prev_ref = await sh.agit("symbolic-ref", "--short", "HEAD") except RuntimeError: - prev_ref = sh.git("rev-parse", "HEAD") + prev_ref = await sh.agit("rev-parse", "HEAD") # If this fails, we don't have to reset - sh.git("checkout", f"{remote_name}/{default_branch}") + await sh.agit("checkout", f"{remote_name}/{default_branch}") try: # Compute the metadata for each commit @@ -134,7 +135,7 @@ def main( # We got this from GitHub, this better not be corrupted assert pr_resolved is not None - ref, closed = lookup_pr_to_orig_ref_and_closed( + ref, closed = await lookup_pr_to_orig_ref_and_closed( github, owner=pr_resolved.owner, name=pr_resolved.repo, @@ -147,16 +148,16 @@ def main( # OK, actually do the land now for orig_ref, pr_resolved in stack_orig_refs: try: - sh.git("cherry-pick", f"{remote_name}/{orig_ref}") + await sh.agit("cherry-pick", f"{remote_name}/{orig_ref}") except BaseException: - sh.git("cherry-pick", "--abort") + await sh.agit("cherry-pick", "--abort") raise # Add PR number to commit message like GitHub does - commit_msg = sh.git("log", "-1", "--pretty=%B") + commit_msg = await sh.agit("log", "-1", "--pretty=%B") # Get the original author and committer dates to preserve the commit hash - author_date = sh.git("log", "-1", "--pretty=%aD") - committer_date = sh.git("log", "-1", "--pretty=%cD") + author_date = await sh.agit("log", "-1", "--pretty=%aD") + committer_date = await sh.agit("log", "-1", "--pretty=%cD") lines = commit_msg.split("\n") if lines: # Add PR number to the subject line (first line) @@ -168,7 +169,7 @@ def main( lines[0] = subject new_msg = "\n".join(lines) # Preserve dates to keep the commit hash consistent - sh.git( + await sh.agit( "commit", "--amend", "-F", @@ -184,7 +185,7 @@ def main( maybe_force_arg = [] if needs_force: maybe_force_arg = ["--force-with-lease"] - sh.git( + await sh.agit( "push", *maybe_force_arg, remote_name, f"HEAD:refs/heads/{default_branch}" ) @@ -203,7 +204,7 @@ def main( # TODO: regex here so janky base_ref = re.sub(r"/orig$", "/base", orig_ref) head_ref = re.sub(r"/orig$", "/head", orig_ref) - sh.git( + await sh.agit( "push", remote_name, f"{remote_name}/{head_ref}:refs/heads/{base_ref}" ) github.notify_merged(pr_resolved) @@ -214,16 +215,16 @@ def main( base_ref = re.sub(r"/orig$", "/base", orig_ref) head_ref = re.sub(r"/orig$", "/head", orig_ref) try: - sh.git("push", remote_name, "--delete", orig_ref, base_ref) + await sh.agit("push", remote_name, "--delete", orig_ref, base_ref) except RuntimeError: # Whatever, keep going logging.warning("Failed to delete branch, continuing", exc_info=True) # Try deleting head_ref separately since often after it's merged it doesn't exist anymore try: - sh.git("push", remote_name, "--delete", head_ref) + await sh.agit("push", remote_name, "--delete", head_ref) except RuntimeError: # Whatever, keep going logging.warning("Failed to delete branch, continuing", exc_info=True) finally: - sh.git("checkout", prev_ref) + await sh.agit("checkout", prev_ref) diff --git a/src/ghstack/log.py b/src/ghstack/log.py index d006e1a..03796a1 100644 --- a/src/ghstack/log.py +++ b/src/ghstack/log.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 -import subprocess +import asyncio import sys from typing import List, Optional, Tuple @@ -10,12 +10,12 @@ import ghstack.shell -def _resolve_refs( +async def _resolve_refs( *, github: ghstack.github.GitHubEndpoint, params: ghstack.github_utils.GitHubPullRequestParams, ) -> Tuple[str, str]: - pr_result = github.graphql( + pr_result = await github.graphql( """ query ($owner: String!, $name: String!, $number: Int!) { repository(name: $name, owner: $owner) { @@ -32,7 +32,7 @@ def _resolve_refs( return pr["headRefName"], pr["baseRefName"] -def main( +async def main( github: ghstack.github.GitHubEndpoint, sh: ghstack.shell.Shell, remote_name: str, @@ -43,15 +43,15 @@ def main( if pull_request is not None: # Explicit PR: fetch from remote and show what's there. HEAD isn't # involved; no synthesized pending-changes commit. - params = ghstack.github_utils.parse_pull_request( + params = await ghstack.github_utils.parse_pull_request( pull_request, sh=sh, remote_name=remote_name ) - sh.git("fetch", "--prune", remote_name) + await sh.agit("fetch", "--prune", remote_name) show_pending = False else: # Infer PR from HEAD's Pull-Request trailer; show the log relative # to the local understanding of the remote state (no fetch). - commit_msg = sh.git("log", "-1", "--format=%B", "HEAD") + commit_msg = await sh.agit("log", "-1", "--format=%B", "HEAD") pr = ghstack.diff.PullRequestResolved.search(commit_msg, github_url) if pr is None: raise RuntimeError( @@ -67,7 +67,7 @@ def main( } show_pending = True - head_ref, base_ref = _resolve_refs(github=github, params=params) + head_ref, base_ref = await _resolve_refs(github=github, params=params) remote_head = f"{remote_name}/{head_ref}" remote_base = f"{remote_name}/{base_ref}" @@ -77,10 +77,10 @@ def main( # If the local HEAD tree differs from the remote head tree, synthesize # a disposable commit on top of the remote head so pending changes # show up as the newest commit in `git log`. - local_tree = sh.git("rev-parse", "HEAD^{tree}") - remote_tree = sh.git("rev-parse", f"{remote_head}^{{tree}}") + local_tree = await sh.agit("rev-parse", "HEAD^{tree}") + remote_tree = await sh.agit("rev-parse", f"{remote_head}^{{tree}}") if local_tree != remote_tree: - tip = sh.git( + tip = await sh.agit( "commit-tree", local_tree, "-p", @@ -99,17 +99,18 @@ def main( # git 2.35+. Users who prefer a portable alternative can pass # --diff-merges=cc to override. log_args = ["--diff-merges=remerge", tip] - if sh.git("rev-parse", "--verify", "--quiet", remote_base, exitcode=True): + if await sh.agit("rev-parse", "--verify", "--quiet", remote_base, exitcode=True): log_args.append(f"^{remote_base}") log_args.extend(args) if sys.stdout.isatty(): # Let git manage its own pager. - subprocess.run(["git", "log", *log_args], cwd=sh.cwd, check=False) + proc = await asyncio.create_subprocess_exec("git", "log", *log_args, cwd=sh.cwd) + await proc.wait() else: # In test/piped contexts, capture and write to sys.stdout so the # caller can intercept it. - out = sh.git("log", *log_args) + out = await sh.agit("log", *log_args) if out: sys.stdout.write(out) if not out.endswith("\n"): diff --git a/src/ghstack/shell.py b/src/ghstack/shell.py index 5cf6933..c2754e0 100644 --- a/src/ghstack/shell.py +++ b/src/ghstack/shell.py @@ -82,38 +82,17 @@ def __init__( self.testing = testing self.testing_time = 1112911993 - def sh( - self, - *args: str, # noqa: C901 - env: Optional[Dict[str, str]] = None, - stderr: _HANDLE = None, - # TODO: Arguably bytes should be accepted here too - input: Optional[str] = None, - stdin: _HANDLE = None, - stdout: _HANDLE = subprocess.PIPE, - exitcode: bool = False, - tick: bool = False, - ) -> _SHELL_RET: - return self._run_async( - self.ash( - *args, - env=env, - stderr=stderr, - input=input, - stdin=stdin, - stdout=stdout, - exitcode=exitcode, - tick=tick, - ) - ) + @overload + async def ash(self, *args: str) -> str: ... + + @overload + async def ash(self, *args: str, input: str) -> str: ... - @staticmethod - def _run_async(coro: Any) -> Any: - loop = asyncio.new_event_loop() - try: - return loop.run_until_complete(coro) - finally: - loop.close() + @overload + async def ash(self, *args: str, input: str, env: Dict[str, str]) -> str: ... + + @overload + async def ash(self, *args: str, stdout: _HANDLE) -> _SHELL_RET: ... async def ash( self, @@ -264,27 +243,17 @@ def _maybe_rstrip(self, s: _SHELL_RET) -> _SHELL_RET: else: return s - @overload # noqa: F811 - def git(self, *args: str) -> str: ... - - @overload # noqa: F811 - def git(self, *args: str, input: str) -> str: ... + @overload + async def agit(self, *args: str) -> str: ... - @overload # noqa: F811 - def git(self, *args: str, input: str, env: Dict[str, str]) -> str: ... + @overload + async def agit(self, *args: str, input: str) -> str: ... - @overload # noqa: F811 - def git(self, *args: str, **kwargs: Any) -> _SHELL_RET: ... + @overload + async def agit(self, *args: str, input: str, env: Dict[str, str]) -> str: ... - def git(self, *args: str, **kwargs: Any) -> _SHELL_RET: # noqa: F811 - """ - Run a git command. The returned stdout has trailing newlines stripped. - - Args: - *args: Arguments to git - **kwargs: Any valid kwargs for sh() - """ - return self._run_async(self.agit(*args, **kwargs)) + @overload + async def agit(self, *args: str, **kwargs: Any) -> _SHELL_RET: ... async def agit(self, *args: str, **kwargs: Any) -> _SHELL_RET: """ @@ -323,25 +292,14 @@ async def agit(self, *args: str, **kwargs: Any) -> _SHELL_RET: return self._maybe_rstrip(await self.ash(*(("git",) + args), **kwargs)) - @overload # noqa: F811 - def hg(self, *args: str) -> str: ... + @overload + async def ahg(self, *args: str) -> str: ... - @overload # noqa: F811 - def hg(self, *args: str, input: str) -> str: ... + @overload + async def ahg(self, *args: str, input: str) -> str: ... - @overload # noqa: F811 - def hg(self, *args: str, **kwargs: Any) -> _SHELL_RET: ... - - def hg(self, *args: str, **kwargs: Any) -> _SHELL_RET: # noqa: F811 - """ - Run a hg command. The returned stdout has trailing newlines stripped. - - Args: - *args: Arguments to hg - **kwargs: Any valid kwargs for sh() - """ - - return self._run_async(self.ahg(*args, **kwargs)) + @overload + async def ahg(self, *args: str, **kwargs: Any) -> _SHELL_RET: ... async def ahg(self, *args: str, **kwargs: Any) -> _SHELL_RET: """ @@ -355,17 +313,6 @@ async def ahg(self, *args: str, **kwargs: Any) -> _SHELL_RET: return self._maybe_rstrip(await self.ash(*(("hg",) + args), **kwargs)) - def jf(self, *args: str, **kwargs: Any) -> _SHELL_RET: - """ - Run a jf command. The returned stdout has trailing newlines stripped. - - Args: - *args: Arguments to jf - **kwargs: Any valid kwargs for sh() - """ - - return self._run_async(self.ajf(*args, **kwargs)) - async def ajf(self, *args: str, **kwargs: Any) -> _SHELL_RET: """ Run a jf command asynchronously. The returned stdout has trailing diff --git a/src/ghstack/status.py b/src/ghstack/status.py index 313f5bf..04beee5 100644 --- a/src/ghstack/status.py +++ b/src/ghstack/status.py @@ -45,7 +45,7 @@ async def main( # to be any indication that a halt was called. So we'll # have to rely on the (OS X jobs, take note!) - params = ghstack.github_utils.parse_pull_request(pull_request) + params = await ghstack.github_utils.parse_pull_request(pull_request) ContextPayload = TypedDict( "ContextPayload", @@ -55,7 +55,7 @@ async def main( "targetUrl": str, }, ) - r = github.graphql( + r = await github.graphql( """ query ($name: String!, $owner: String!, $number: Int!) { repository(name: $name, owner: $owner) { diff --git a/src/ghstack/submit.py b/src/ghstack/submit.py index ed51241..22f3d94 100644 --- a/src/ghstack/submit.py +++ b/src/ghstack/submit.py @@ -292,19 +292,12 @@ def report(self) -> None: logging.info("[ghstack timing] total: %.0fms", total * 1000) -def _run_async_ordered(awaitables: Iterable[Awaitable[_T]]) -> List[_T]: +async def _gather_ordered(awaitables: Iterable[Awaitable[_T]]) -> List[_T]: aws = list(awaitables) if not aws: return [] - async def gather() -> List[Any]: - return await asyncio.gather(*aws, return_exceptions=True) - - loop = asyncio.new_event_loop() - try: - results = loop.run_until_complete(gather()) - finally: - loop.close() + results = await asyncio.gather(*aws, return_exceptions=True) checked_results: List[_T] = [] for result in results: @@ -324,9 +317,10 @@ class _PendingNewPR: diff_meta: "DiffMeta" -def main(**kwargs: Any) -> List[DiffMeta]: +async def main(**kwargs: Any) -> List[DiffMeta]: submitter = Submitter(**kwargs) - return submitter.run() + await submitter.initialize() + return await submitter.run() def all_branches(username: str, ghnum: GhNumber) -> Tuple[str, str, str]: @@ -450,12 +444,16 @@ class Submitter: # Set of seen ghnums seen_ghnums: Set[Tuple[str, GhNumber]] = dataclasses.field(default_factory=set) + _pending_new_prs: List[_PendingNewPR] = dataclasses.field( + default_factory=list, init=False + ) + # ~~~~~~~~~~~~~~~~~~~~~~~~ # Post initialization - def __post_init__(self) -> None: + async def initialize(self) -> None: # Network call in the constructor, help me father, for I have sinned - repo = ghstack.github_utils.get_github_repo_info( + repo = await ghstack.github_utils.get_github_repo_info( github=self.github, sh=self.sh, repo_owner=self.repo_owner_opt, @@ -489,7 +487,7 @@ def __post_init__(self) -> None: # specify an option direct = self.direct_opt if direct is None: - direct_r = self.sh.git( + direct_r = await self.sh.agit( "cat-file", "-e", "HEAD:.github/ghstack_direct", exitcode=True ) assert isinstance(direct_r, bool) @@ -500,7 +498,7 @@ def __post_init__(self) -> None: # ~~~~~~~~~~~~~~~~~~~~~~~~ # The main algorithm - def run(self) -> List[DiffMeta]: + async def run(self) -> List[DiffMeta]: timer = _Timer() if _TIMING_ENABLED else None if not self.no_fetch: @@ -510,14 +508,14 @@ def run(self) -> List[DiffMeta]: # base ref before rebasing. The later rev-list boundary is against # that local base ref, so narrowing this fetch avoids unrelated # remote IO while preserving the usual submit semantics. - self.fetch( + await self.fetch( f"+refs/heads/gh/{self.username}/*" f":refs/remotes/{self.remote_name}/gh/{self.username}/*" ) if timer: timer.mark("fetch") - commits_to_submit_and_boundary = self.parse_revs() + commits_to_submit_and_boundary = await self.parse_revs() commits_to_submit = [ d for d in commits_to_submit_and_boundary if not d.boundary @@ -528,7 +526,7 @@ def run(self) -> List[DiffMeta]: # also parse prefix even if it's not being processed, but it's at most ~10 # extra parses so whatever commits_to_rebase_and_boundary = ghstack.git.split_header( - self.sh.git( + await self.sh.agit( "rev-list", "--boundary", "--header", @@ -565,13 +563,13 @@ def run(self) -> List[DiffMeta]: # This is not really accurate if you're doing a fancy pattern; # if this is a problem file us a bug. - run_pre_ghstack_hook( + await run_pre_ghstack_hook( self.sh, f"{self.remote_name}/{self.base}", commits_to_submit[0].commit_id ) - pr_info_cache = self._prefetch_pr_info(commits_to_rebase) + pr_info_cache = await self._prefetch_pr_info(commits_to_rebase) if not self.no_fetch: - self._fetch_foreign_pr_refs(pr_info_cache.values()) + await self._fetch_foreign_pr_refs(pr_info_cache.values()) # NB: This is duplicative with prepare_submit to keep the # check_invariants code small, as it counts as TCB @@ -580,7 +578,7 @@ def run(self) -> List[DiffMeta]: for h in commits_to_submit: d = ghstack.git.convert_header(h, self.github_url) if d.pull_request_resolved is not None: - ed = self.elaborate_diff( + ed = await self.elaborate_diff( d, _pr_info=pr_info_cache.get(d.pull_request_resolved.number), ) @@ -588,12 +586,12 @@ def run(self) -> List[DiffMeta]: if not ed.closed: pre_branch_state_index[h.commit_id] = PreBranchState( head_commit_id=GitCommitHash( - self.sh.git( + await self.sh.agit( "rev-parse", f"{self.remote_name}/{ed.head_ref}" ) ), base_commit_id=GitCommitHash( - self.sh.git( + await self.sh.agit( "rev-parse", f"{self.remote_name}/{ed.base_ref}" ) ), @@ -606,7 +604,7 @@ def run(self) -> List[DiffMeta]: commits_to_submit_and_boundary, commits_to_rebase_and_boundary ) } - diff_meta_index, rebase_index = self.prepare_updates( + diff_meta_index, rebase_index = await self.prepare_updates( commit_index, commits_to_submit, commits_to_rebase, @@ -625,24 +623,24 @@ def run(self) -> List[DiffMeta]: for h in commits_to_rebase if h.commit_id in diff_meta_index ] - self.push_updates(diffs_to_submit, all_diffs=all_diffs_in_topo_order) + await self.push_updates(diffs_to_submit, all_diffs=all_diffs_in_topo_order) if timer: timer.mark("push_updates") if new_head := rebase_index.get( - old_head := GitCommitHash(self.sh.git("rev-parse", "HEAD")) + old_head := GitCommitHash(await self.sh.agit("rev-parse", "HEAD")) ): - self.sh.git("reset", "--soft", new_head) + await self.sh.agit("reset", "--soft", new_head) # TODO: print out commit hashes for things we rebased but not accessible # from HEAD if self.check_invariants: - self.fetch() + await self.fetch() for h in commits_to_submit: # TODO: Do a separate check for this if h.commit_id not in diff_meta_index: continue new_orig = diff_meta_index[h.commit_id].orig - self.check_invariants_for_diff( + await self.check_invariants_for_diff( h.commit_id, new_orig, pre_branch_state_index.get(h.commit_id), @@ -650,16 +648,16 @@ def run(self) -> List[DiffMeta]: # Test that orig commits are accessible from HEAD, if the old # commits were accessible. And if the commit was not # accessible, it better not be accessible now! - if self.sh.git( + if await self.sh.agit( "merge-base", "--is-ancestor", h.commit_id, old_head, exitcode=True ): assert new_head is not None - assert self.sh.git( + assert await self.sh.agit( "merge-base", "--is-ancestor", new_orig, new_head, exitcode=True ) else: if new_head is not None: - assert not self.sh.git( + assert not await self.sh.agit( "merge-base", "--is-ancestor", new_orig, @@ -677,23 +675,23 @@ def run(self) -> List[DiffMeta]: # ~~~~~~~~~~~~~~~~~~~~~~~~ # The main pieces - def fetch(self, refspec: Optional[str] = None) -> None: + async def fetch(self, refspec: Optional[str] = None) -> None: if refspec is not None: - self.sh.git( + await self.sh.agit( "fetch", "--prune", self.remote_name, refspec, ) else: - self.sh.git( + await self.sh.agit( "fetch", "--prune", self.remote_name, f"+refs/heads/*:refs/remotes/{self.remote_name}/*", ) - def parse_revs(self) -> List[ghstack.git.CommitHeader]: + async def parse_revs(self) -> List[ghstack.git.CommitHeader]: # There are two distinct usage patterns: # # 1. You may want to submit only HEAD, but not everything below it, @@ -755,7 +753,7 @@ def parse_revs(self) -> List[ghstack.git.CommitHeader]: # Easy case, make rev-list do the hard work commits_to_submit_and_boundary.extend( ghstack.git.split_header( - self.sh.git( + await self.sh.agit( "rev-list", "--header", "--topo-order", @@ -770,7 +768,7 @@ def parse_revs(self) -> List[ghstack.git.CommitHeader]: for rev in revs: # We still do rev-list as it gets us the parent commits r = ghstack.git.split_header( - self.sh.git( + await self.sh.agit( "rev-list", "--header", "--topo-order", @@ -791,7 +789,7 @@ def parse_revs(self) -> List[ghstack.git.CommitHeader]: return commits_to_submit_and_boundary - def _prefetch_pr_info( + async def _prefetch_pr_info( self, commits: List[ghstack.git.CommitHeader], ) -> Dict[GitHubNumber, Any]: @@ -815,12 +813,12 @@ async def fetch_pr(number: GitHubNumber) -> Tuple[GitHubNumber, Any]: ) return number, r - results = _run_async_ordered(fetch_pr(number) for number in unique_numbers) + results = await _gather_ordered(fetch_pr(number) for number in unique_numbers) pr_info: Dict[GitHubNumber, Any] = dict(results) return pr_info - def _fetch_foreign_pr_refs(self, pr_infos: Iterable[Any]) -> None: + async def _fetch_foreign_pr_refs(self, pr_infos: Iterable[Any]) -> None: usernames: Set[str] = set() for pr_info in pr_infos: head_ref_name = self._pr_ref_name(pr_info, "head") @@ -834,12 +832,12 @@ def _fetch_foreign_pr_refs(self, pr_infos: Iterable[Any]) -> None: # TODO: Potentially we could narrow this refspec down to only the # referenced PRs. However, this will interact poorly with # cross-author stacks, so it needs to be thought more carefully. - self.fetch( + await self.fetch( f"+refs/heads/gh/{username}/*" f":refs/remotes/{self.remote_name}/gh/{username}/*" ) - def prepare_updates( + async def prepare_updates( self, commit_index: Dict[GitCommitHash, ghstack.git.CommitHeader], commits_to_submit: List[ghstack.git.CommitHeader], @@ -849,7 +847,7 @@ def prepare_updates( ) -> Tuple[Dict[GitCommitHash, DiffMeta], Dict[GitCommitHash, GitCommitHash]]: # Prefetch PR info for all commits with existing PRs (parallel REST GETs) if pr_info_cache is None: - pr_info_cache = self._prefetch_pr_info(commits_to_rebase) + pr_info_cache = await self._prefetch_pr_info(commits_to_rebase) # Phase 1: Process all commits (oldest first) to determine what # needs updating, create head/base commits, and identify new PRs. @@ -872,12 +870,12 @@ def prepare_updates( parent_commit = commit_index[parent] parent_diff_meta = diff_meta_index.get(parent) diff = ghstack.git.convert_header(commit, self.github_url) - diff_meta = self.process_commit( + diff_meta = await self.process_commit( parent_commit, parent_diff_meta, diff, ( - self.elaborate_diff( + await self.elaborate_diff( diff, _pr_info=pr_info_cache.get(diff.pull_request_resolved.number), ) @@ -897,14 +895,14 @@ def prepare_updates( for pending in pending_new_prs: all_new_push_specs.extend(pending.push_specs) if all_new_push_specs: - self._git_push(all_new_push_specs) + await self._git_push(all_new_push_specs) # Create PRs in stack order. GitHub PR numbers are globally allocated, # so parallel creation makes numbering nondeterministic. results = [ ( pending, - self._create_pull_request( + await self._create_pull_request( pending.diff, pending.base_diff_meta, pending.ghnum, @@ -963,9 +961,9 @@ def prepare_updates( env["GIT_AUTHOR_EMAIL"] = commit.author_email new_orig = GitCommitHash( - self.sh.git( + await self.sh.agit( "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), + *(await ghstack.gpg_sign.gpg_args_if_necessary(self.sh)), "-p", base_commit_id, commit.tree, @@ -983,7 +981,7 @@ def prepare_updates( return diff_meta_index, rebase_index - def elaborate_diff( + async def elaborate_diff( self, diff: ghstack.diff.Diff, *, @@ -1003,7 +1001,7 @@ def elaborate_diff( # Use pre-fetched PR info if available, otherwise fetch now. if _pr_info is None: - pr_info = self.github.get( + pr_info = await self.github.aget( f"repos/{self.repo_owner}/{self.repo_name}/pulls/{number}" ) else: @@ -1011,7 +1009,7 @@ def elaborate_diff( head_ref_name = self._pr_ref_name(pr_info, "head") if head_ref_name is None: - head_ref_name = self.github.get_head_ref( + head_ref_name = await self.github.get_head_ref( owner=self.repo_owner, name=self.repo_name, number=number ) @@ -1074,7 +1072,7 @@ def elaborate_diff( try: # TODO: remote summary should be done earlier so we can use # it to test if updates are necessary - rev_list = self.sh.git( + rev_list = await self.sh.agit( "rev-list", "--max-count=1", "--header", @@ -1121,7 +1119,7 @@ def _pr_ref_name(self, pr_info: Any, kind: str) -> Optional[str]: return name return None - def process_commit( + async def process_commit( self, base: ghstack.git.CommitHeader, base_diff_meta: Optional[DiffMeta], @@ -1147,7 +1145,7 @@ def process_commit( # Otherwise, it's been modified and we should raise an error. try: # Check if there's a commit on main with the same tree (source_id) - main_commits = self.sh.git( + main_commits = await self.sh.agit( "log", "--format=%H %T", f"{self.remote_name}/{self.base}", @@ -1191,11 +1189,13 @@ def process_commit( return None username = elab_diff.username if elab_diff is not None else self.username - ghnum = elab_diff.ghnum if elab_diff is not None else self._allocate_ghnum() + ghnum = ( + elab_diff.ghnum if elab_diff is not None else await self._allocate_ghnum() + ) self._sanity_check_ghnum(username, ghnum) # Create base/head commits if needed - push_branches, base_branch = self._create_non_orig_branches( + push_branches, base_branch = await self._create_non_orig_branches( base, base_diff_meta, diff, elab_diff, username, ghnum, submit ) @@ -1222,7 +1222,7 @@ def process_commit( pull_request_resolved=ghstack.diff.PullRequestResolved( owner=self.repo_owner, repo=self.repo_name, - number=0, + number=GitHubNumber(0), github_url=self.github_url, ), head_ref=str(branch_head(username, ghnum)), @@ -1240,7 +1240,7 @@ def process_commit( ) pending_new_prs.append( _PendingNewPR( - commit_id=diff.oid, + commit_id=GitCommitHash(diff.oid), diff=diff, base_diff_meta=base_diff_meta, ghnum=ghnum, @@ -1299,7 +1299,7 @@ def _warn_empty( "Skipping '{}', as the commit now has no changes".format(diff.title) ) - def _allocate_ghnum(self) -> GhNumber: + async def _allocate_ghnum(self) -> GhNumber: # Check both seen_ghnums (from commits in the current stack) and # remote refs (which may include ghnums from landed/closed PRs # whose branches still exist) @@ -1313,10 +1313,12 @@ def _allocate_ghnum(self) -> GhNumber: ), default=0, ) - refs = self.sh.git( - "for-each-ref", - "refs/remotes/{}/gh/{}".format(self.remote_name, self.username), - "--format=%(refname)", + refs = ( + await self.sh.agit( + "for-each-ref", + "refs/remotes/{}/gh/{}".format(self.remote_name, self.username), + "--format=%(refname)", + ) ).split() max_ref = 0 for ref in refs: @@ -1383,27 +1385,27 @@ def _update_source_id(self, summary: str, elab_diff: DiffWithGitHubMetadata) -> return summary # NB: mutates GhBranch - def _resolve_gh_branch( + async def _resolve_gh_branch( self, kind: str, gh_branch: GhBranch, username: str, ghnum: GhNumber ) -> None: remote_ref = self.remote_name + "/" + branch(username, ghnum, kind) (remote_commit,) = ghstack.git.split_header( - self.sh.git("rev-list", "--header", "-1", remote_ref) + await self.sh.agit("rev-list", "--header", "-1", remote_ref) ) gh_branch.commit = GhCommit(remote_commit.commit_id, remote_commit.tree) # Precondition: these branches exist - def _resolve_gh_branches(self, username: str, ghnum: GhNumber) -> GhBranches: + async def _resolve_gh_branches(self, username: str, ghnum: GhNumber) -> GhBranches: push_branches = GhBranches() - self._resolve_gh_branch("orig", push_branches.orig, username, ghnum) - self._resolve_gh_branch("head", push_branches.head, username, ghnum) + await self._resolve_gh_branch("orig", push_branches.orig, username, ghnum) + await self._resolve_gh_branch("head", push_branches.head, username, ghnum) if self.direct: - self._resolve_gh_branch("next", push_branches.next, username, ghnum) + await self._resolve_gh_branch("next", push_branches.next, username, ghnum) else: - self._resolve_gh_branch("base", push_branches.base, username, ghnum) + await self._resolve_gh_branch("base", push_branches.base, username, ghnum) return push_branches - def _create_non_orig_branches( + async def _create_non_orig_branches( self, base: ghstack.git.CommitHeader, base_diff_meta: Optional[DiffMeta], @@ -1470,7 +1472,7 @@ def _create_non_orig_branches( # is updated to also remove changes. if elab_diff is not None: - push_branches = self._resolve_gh_branches(username, ghnum) + push_branches = await self._resolve_gh_branches(username, ghnum) else: push_branches = GhBranches() @@ -1500,10 +1502,10 @@ def _create_non_orig_branches( # incorporate changes on base, and if a ghstack has been # rebased backwards in time, the merge-base will be stuck # on the more recent commit), it is useful so we put it in. - extra_base = self.sh.git( + extra_base = await self.sh.agit( "merge-base", base.commit_id, f"{self.remote_name}/{self.base}" ) - if push_branches.base.commit is None or not self.sh.git( + if push_branches.base.commit is None or not await self.sh.agit( "merge-base", "--is-ancestor", extra_base, @@ -1512,9 +1514,9 @@ def _create_non_orig_branches( ): base_args.extend(("-p", extra_base)) new_base = GitCommitHash( - self.sh.git( + await self.sh.agit( "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), + *(await ghstack.gpg_sign.gpg_args_if_necessary(self.sh)), *base_args, base.tree, input="{} (base update)\n\n[ghstack-poisoned]".format(self.msg), @@ -1647,7 +1649,7 @@ def _create_non_orig_branches( # Check if the base is already an ancestor, don't need to add it # if so - if push_branches.next.commit is not None and self.sh.git( + if push_branches.next.commit is not None and await self.sh.agit( "merge-base", "--is-ancestor", new_base, @@ -1667,9 +1669,9 @@ def _create_non_orig_branches( or push_branches.head.commit.tree != diff.tree ): new_head = GitCommitHash( - self.sh.git( + await self.sh.agit( "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(self.sh), + *(await ghstack.gpg_sign.gpg_args_if_necessary(self.sh)), *head_args, diff.tree, input="{}\n\n[ghstack-poisoned]".format(self.msg), @@ -1685,7 +1687,7 @@ def _create_non_orig_branches( return push_branches, base_branch - def _create_pull_request( + async def _create_pull_request( self, diff: ghstack.diff.Diff, base_diff_meta: Optional[DiffMeta], @@ -1704,7 +1706,7 @@ def _create_pull_request( # Time to open the PR # NB: GraphQL API does not support opening PRs - r = self.github.post( + r = await self.github.apost( "repos/{owner}/{repo}/pulls".format( owner=self.repo_owner, repo=self.repo_name ), @@ -1719,7 +1721,7 @@ def _create_pull_request( comment_id = None if self.direct: - rc = self.github.post( + rc = await self.github.apost( f"repos/{self.repo_owner}/{self.repo_name}/issues/{number}/comments", body=f"{self.stack_header}:\n* (to be filled)", ) @@ -1730,7 +1732,7 @@ def _create_pull_request( reviewers = [r.strip() for r in self.reviewer.split(",") if r.strip()] if reviewers: try: - self.github.post( + await self.github.apost( f"repos/{self.repo_owner}/{self.repo_name}/pulls/{number}/requested_reviewers", reviewers=reviewers, ) @@ -1743,7 +1745,7 @@ def _create_pull_request( labels = [label.strip() for label in self.label.split(",") if label.strip()] if labels: try: - self.github.post( + await self.github.apost( f"repos/{self.repo_owner}/{self.repo_name}/issues/{number}/labels", labels=labels, ) @@ -1775,7 +1777,7 @@ def _create_pull_request( base_ref=base_ref, ) - def push_updates( + async def push_updates( self, diffs_to_submit: List[DiffMeta], *, @@ -1812,7 +1814,7 @@ def push_updates( push_spec(diff, branch(s.username, s.ghnum, b), force=force) ) if all_push_specs: - self._git_push(all_push_specs) + await self._git_push(all_push_specs) # Discover orphan PR numbers from the old stack listing. # We search the full local stack for old stack text, then @@ -1824,7 +1826,7 @@ def push_updates( for s in all_diffs or diffs_to_submit: old_stack_text: Optional[str] = None if self.direct and s.elab_diff.comment_id is not None: - r = self.github.get( + r = await self.github.aget( f"repos/{self.repo_owner}/{self.repo_name}/issues/comments/{s.elab_diff.comment_id}", ) old_stack_text = r.get("body") @@ -1844,7 +1846,7 @@ def push_updates( if num in submitted_numbers: seen_submitted = True continue - pr_info = self.github.get( + pr_info = await self.github.aget( f"repos/{self.repo_owner}/{self.repo_name}/pulls/{num}", ) if pr_info.get("state") == "open": @@ -1905,7 +1907,7 @@ async def _update_pr_async(s: DiffMeta) -> None: ), ) - _run_async_ordered(_update_pr_async(s) for s in reversed(diffs_to_submit)) + await _gather_ordered(_update_pr_async(s) for s in reversed(diffs_to_submit)) # Report what happened def format_url(s: DiffMeta) -> str: @@ -1968,7 +1970,7 @@ def format_url(s: DiffMeta) -> str: "I did NOT close or update PRs previously associated with these commits." ) - def check_invariants_for_diff( + async def check_invariants_for_diff( self, # the user diff is what the user actual sent us user_commit_id: GitCommitHash, @@ -1990,13 +1992,17 @@ def assert_eq(a: Any, b: Any) -> None: # Fetch information about user/orig commits, do some basic sanity # checks user_commit, user_parent_commit = ghstack.git.split_header( - self.sh.git("rev-list", "--header", "--boundary", "-1", user_commit_id) + await self.sh.agit( + "rev-list", "--header", "--boundary", "-1", user_commit_id + ) ) assert_eq(user_commit.commit_id, user_commit_id) assert not user_commit.boundary assert user_parent_commit.boundary orig_commit, orig_parent_commit = ghstack.git.split_header( - self.sh.git("rev-list", "--header", "--boundary", "-1", orig_commit_id) + await self.sh.agit( + "rev-list", "--header", "--boundary", "-1", orig_commit_id + ) ) assert_eq(orig_commit.commit_id, orig_commit_id) assert not orig_commit.boundary @@ -2021,13 +2027,15 @@ def assert_eq(a: Any, b: Any) -> None: assert m is not None assert_eq(m.group(1), orig_commit.tree) - elaborated_orig_diff = self.elaborate_diff(orig_diff) + elaborated_orig_diff = await self.elaborate_diff(orig_diff) # 5. GitHub branches are correct head_ref = elaborated_orig_diff.head_ref assert_eq(head_ref, branch_head(self.username, elaborated_orig_diff.ghnum)) (head_commit,) = ghstack.git.split_header( - self.sh.git("rev-list", "--header", "-1", f"{self.remote_name}/{head_ref}") + await self.sh.agit( + "rev-list", "--header", "-1", f"{self.remote_name}/{head_ref}" + ) ) assert_eq(head_commit.tree, user_commit.tree) @@ -2040,7 +2048,9 @@ def assert_eq(a: Any, b: Any) -> None: pass (base_commit,) = ghstack.git.split_header( - self.sh.git("rev-list", "--header", "-1", f"{self.remote_name}/{base_ref}") + await self.sh.agit( + "rev-list", "--header", "-1", f"{self.remote_name}/{base_ref}" + ) ) # TODO: tree equality may not hold for self.direct, figure out a # related invariant @@ -2051,7 +2061,7 @@ def assert_eq(a: Any, b: Any) -> None: assert_eq( orig_commit.commit_id, GitCommitHash( - self.sh.git( + await self.sh.agit( "rev-parse", self.remote_name + "/" @@ -2083,7 +2093,7 @@ def assert_eq(a: Any, b: Any) -> None: # assert not base_commit.parents # 8. Head branch is not malformed - assert self.sh.git( + assert await self.sh.agit( "merge-base", "--is-ancestor", base_commit.commit_id, @@ -2172,10 +2182,10 @@ def _default_title_and_body( ) return title, pr_body - def _git_push(self, branches: Sequence[str], force: bool = False) -> None: + async def _git_push(self, branches: Sequence[str], force: bool = False) -> None: assert branches, "empty branches would push main, probably bad!" try: - self.sh.git( + await self.sh.agit( "push", self.remote_name, "--no-verify", @@ -2183,7 +2193,9 @@ def _git_push(self, branches: Sequence[str], force: bool = False) -> None: *branches, ) except RuntimeError as e: - remote_url = self.sh.git("remote", "get-url", "--push", self.remote_name) + remote_url = await self.sh.agit( + "remote", "get-url", "--push", self.remote_name + ) if remote_url.startswith("https://"): raise RuntimeError( "[E001] git push failed, probably because it asked for password " @@ -2195,15 +2207,15 @@ def _git_push(self, branches: Sequence[str], force: bool = False) -> None: self.github.push_hook(branches) -def run_pre_ghstack_hook( +async def run_pre_ghstack_hook( sh: ghstack.shell.Shell, base_commit: str, top_commit: str ) -> None: """If a `pre-ghstack` git hook is configured, run it.""" default_hooks_path = os.path.join( - sh.git("rev-parse", "--show-toplevel"), ".git/hooks" + await sh.agit("rev-parse", "--show-toplevel"), ".git/hooks" ) try: - hooks_path = sh.git( + hooks_path = await sh.agit( "config", "--default", default_hooks_path, "--get", "core.hooksPath" ) hook_file = os.path.join(hooks_path, "pre-ghstack") @@ -2214,4 +2226,4 @@ def run_pre_ghstack_hook( if not os.path.isfile(hook_file) or not os.access(hook_file, os.X_OK): return - sh.sh(hook_file, base_commit, top_commit, stdout=None) + await sh.ash(hook_file, base_commit, top_commit, stdout=None) diff --git a/src/ghstack/sync.py b/src/ghstack/sync.py index 4aa16bc..96869bb 100644 --- a/src/ghstack/sync.py +++ b/src/ghstack/sync.py @@ -16,7 +16,7 @@ RE_STACK = re.compile(r"Stack.*:\r?\n(\* [^\r\n]+\r?\n)+") -def main( +async def main( *, github: ghstack.github.GitHubEndpoint, sh: ghstack.shell.Shell, @@ -25,7 +25,7 @@ def main( github_url: str, remote_name: str, ) -> GitCommitHash: - repo_info = ghstack.github_utils.get_github_repo_info( + repo_info = await ghstack.github_utils.get_github_repo_info( github=github, sh=sh, repo_owner=repo_owner, @@ -38,11 +38,11 @@ def main( default_branch = repo_info["default_branch"] base = GitCommitHash( - sh.git("merge-base", f"{remote_name}/{default_branch}", "HEAD") + await sh.agit("merge-base", f"{remote_name}/{default_branch}", "HEAD") ) stack = ghstack.git.split_header( - sh.git("rev-list", "--reverse", "--header", "^" + base, "HEAD") + await sh.agit("rev-list", "--reverse", "--header", "^" + base, "HEAD") ) if not stack: @@ -59,9 +59,9 @@ def main( head = s.commit_id else: head = GitCommitHash( - sh.git( + await sh.agit( "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(sh), + *(await ghstack.gpg_sign.gpg_args_if_necessary(sh)), s.tree, "-p", head, @@ -74,7 +74,7 @@ def main( assert pr.owner == repo_owner assert pr.repo == repo_name - r = github.graphql( + pr_result = await github.graphql( """ query ($owner: String!, $name: String!, $number: Int!) { repository(owner: $owner, name: $name) { @@ -88,7 +88,8 @@ def main( owner=repo_owner, name=repo_name, number=pr.number, - )["data"]["repository"]["pullRequest"] + ) + r = pr_result["data"]["repository"]["pullRequest"] pr_title = r["title"] pr_body = r["body"] @@ -116,9 +117,9 @@ def main( logging.debug("-- old commit_msg:\n%s", textwrap.indent(s.commit_msg, " ")) logging.debug("-- new commit_msg:\n%s", textwrap.indent(new_msg, " ")) head = GitCommitHash( - sh.git( + await sh.agit( "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(sh), + *(await ghstack.gpg_sign.gpg_args_if_necessary(sh)), s.tree, "-p", head, @@ -127,7 +128,7 @@ def main( ) if rewriting: - sh.git("reset", "--soft", head) + await sh.agit("reset", "--soft", head) logging.info( "\nCommit messages successfully synced from PR descriptions!\n\n" "To undo this operation, run:\n\n" diff --git a/src/ghstack/test_prelude.py b/src/ghstack/test_prelude.py index 0607167..4b05387 100644 --- a/src/ghstack/test_prelude.py +++ b/src/ghstack/test_prelude.py @@ -2,13 +2,25 @@ import atexit import contextlib import io +import inspect import os import re import shutil import stat import sys import tempfile -from typing import Any, Callable, Iterator, List, Optional, Sequence, Tuple, Type, Union +from typing import ( + Any, + AsyncIterator, + Callable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Type, + Union, +) from expecttest import assert_expected_inline @@ -118,10 +130,14 @@ def __init__(self, direct: bool) -> None: local_dir = tempfile.mkdtemp() self.sh = ghstack.shell.Shell(cwd=local_dir, testing=True) - self.sh.git("clone", upstream_dir, ".") - self.sh.git("fetch", "origin", "+refs/heads/*:refs/remotes/origin/*") self.direct = direct + async def initialize(self) -> None: + assert isinstance(self.github, ghstack.github_fake.FakeGitHubEndpoint) + await self.github.state.initialize() + await self.sh.agit("clone", self.upstream_sh.cwd, ".") + await self.sh.agit("fetch", "origin", "+refs/heads/*:refs/remotes/origin/*") + def cleanup(self) -> None: if GH_KEEP_TMP: print("upstream_dir preserved at: {}".format(self.upstream_sh.cwd)) @@ -136,8 +152,8 @@ def cleanup(self) -> None: onerror=handle_remove_read_only, ) - def check_global_github_invariants(self, direct: bool) -> None: - r = self.github.graphql( + async def check_global_github_invariants(self, direct: bool) -> None: + r = await self.github.graphql( """ query { repository(name: "pytorch", owner: "pytorch") { @@ -169,23 +185,25 @@ def check_global_github_invariants(self, direct: bool) -> None: CTX: Context = None # type: ignore -def init_test() -> Context: +async def init_test() -> Context: global CTX if CTX is None: parser = argparse.ArgumentParser() parser.add_argument("--direct", action="store_true") args = parser.parse_args() CTX = Context(args.direct) + await CTX.initialize() atexit.register(CTX.cleanup) return CTX -@contextlib.contextmanager -def scoped_test(direct: bool) -> Iterator[None]: +@contextlib.asynccontextmanager +async def scoped_test(direct: bool) -> AsyncIterator[None]: global CTX assert CTX is None try: CTX = Context(direct) + await CTX.initialize() yield finally: CTX.cleanup() @@ -193,7 +211,7 @@ def scoped_test(direct: bool) -> Iterator[None]: # NB: returns earliest first -def gh_submit( +async def gh_submit( msg: str = "Update", update_fields: bool = False, short: bool = False, @@ -205,7 +223,7 @@ def gh_submit( label: Optional[str] = None, ) -> List[ghstack.submit.DiffMeta]: self = CTX - r = ghstack.submit.main( + r = await ghstack.submit.main( msg=msg, username="ezyang", github=self.github, @@ -226,13 +244,13 @@ def gh_submit( reviewer=reviewer, label=label, ) - self.check_global_github_invariants(self.direct) + await self.check_global_github_invariants(self.direct) return r -def gh_land(pull_request: str) -> None: +async def gh_land(pull_request: str) -> None: self = CTX - return ghstack.land.main( + return await ghstack.land.main( remote_name="origin", pull_request=pull_request, github=self.github, @@ -241,9 +259,9 @@ def gh_land(pull_request: str) -> None: ) -def gh_unlink() -> None: +async def gh_unlink() -> None: self = CTX - ghstack.unlink.main( + await ghstack.unlink.main( github=self.github, sh=self.sh, repo_owner="pytorch", @@ -253,9 +271,9 @@ def gh_unlink() -> None: ) -def gh_cherry_pick(pull_request: str, stack: bool = False) -> None: +async def gh_cherry_pick(pull_request: str, stack: bool = False) -> None: self = CTX - return ghstack.cherry_pick.main( + return await ghstack.cherry_pick.main( pull_request=pull_request, github=self.github, sh=self.sh, @@ -264,9 +282,9 @@ def gh_cherry_pick(pull_request: str, stack: bool = False) -> None: ) -def gh_checkout(pull_request: str, same_base: bool = False) -> None: +async def gh_checkout(pull_request: str, same_base: bool = False) -> None: self = CTX - return ghstack.checkout.main( + return await ghstack.checkout.main( pull_request=pull_request, github=self.github, sh=self.sh, @@ -275,9 +293,9 @@ def gh_checkout(pull_request: str, same_base: bool = False) -> None: ) -def gh_log(pull_request: Optional[str] = None, args: Sequence[str] = ()) -> None: +async def gh_log(pull_request: Optional[str] = None, args: Sequence[str] = ()) -> None: self = CTX - return ghstack.log.main( + return await ghstack.log.main( github=self.github, sh=self.sh, remote_name="origin", @@ -287,9 +305,9 @@ def gh_log(pull_request: Optional[str] = None, args: Sequence[str] = ()) -> None ) -def gh_sync() -> GitCommitHash: +async def gh_sync() -> GitCommitHash: self = CTX - return ghstack.sync.main( + return await ghstack.sync.main( github=self.github, sh=self.sh, repo_owner="pytorch", @@ -299,17 +317,17 @@ def gh_sync() -> GitCommitHash: ) -def write_file_and_add(filename: str, contents: str) -> None: +async def write_file_and_add(filename: str, contents: str) -> None: self = CTX with self.sh.open(filename, "w") as f: f.write(contents) - self.sh.git("add", filename) + await self.sh.agit("add", filename) -def commit(name: str, msg: Optional[str] = None) -> None: +async def commit(name: str, msg: Optional[str] = None) -> None: self = CTX - write_file_and_add(f"{name}.txt", "A") - self.sh.git( + await write_file_and_add(f"{name}.txt", "A") + await self.sh.agit( "commit", "-m", f"Commit {name}\n\nThis is commit {name}" if msg is None else msg, @@ -317,41 +335,41 @@ def commit(name: str, msg: Optional[str] = None) -> None: self.sh.test_tick() -def amend(name: str) -> None: +async def amend(name: str) -> None: self = CTX - write_file_and_add(f"{name}.txt", "A") - self.sh.git("commit", "--amend", "--no-edit", tick=True) + await write_file_and_add(f"{name}.txt", "A") + await self.sh.agit("commit", "--amend", "--no-edit", tick=True) -def git(*args: Any, **kwargs: Any) -> Any: - return CTX.sh.git(*args, **kwargs) +async def git(*args: Any, **kwargs: Any) -> Any: + return await CTX.sh.agit(*args, **kwargs) def ok() -> None: print("\033[92m" + "TEST PASSED" + "\033[0m") -def checkout(commit: Union[GitCommitHash, ghstack.submit.DiffMeta]) -> None: +async def checkout(commit: Union[GitCommitHash, ghstack.submit.DiffMeta]) -> None: self = CTX if isinstance(commit, ghstack.submit.DiffMeta): h = commit.orig else: h = commit - self.sh.git("checkout", h) + await self.sh.agit("checkout", h) -def cherry_pick(commit: Union[GitCommitHash, ghstack.submit.DiffMeta]) -> None: +async def cherry_pick(commit: Union[GitCommitHash, ghstack.submit.DiffMeta]) -> None: self = CTX if isinstance(commit, ghstack.submit.DiffMeta): h = commit.orig else: h = commit - self.sh.git("cherry-pick", h, tick=True) + await self.sh.agit("cherry-pick", h, tick=True) -def dump_github() -> str: +async def dump_github() -> str: self = CTX - r = self.github.graphql( + r = await self.github.graphql( """ query { repository(name: "pytorch", owner: "pytorch") { @@ -383,7 +401,7 @@ def dump_github() -> str: # puts the first parent on the left, which leads to ugly # graphs. Swapping the parents would give us nice pretty graphs. if not pr["closed"]: - pr["commits"] = self.upstream_sh.git( + pr["commits"] = await self.upstream_sh.agit( "log", "--graph", "--oneline", @@ -399,7 +417,7 @@ def dump_github() -> str: "{body}\n\n{commits}\n\n".format(**pr) ) - refs = self.upstream_sh.git( + refs = await self.upstream_sh.agit( "log", "--graph", "--oneline", @@ -413,8 +431,8 @@ def dump_github() -> str: return indent("".join(prs), " " * 8) + " " * 8 -def assert_github_state(expect: str, *, skip: int = 0) -> None: - assert_expected_inline(dump_github(), expect, skip=skip + 1) +async def assert_github_state(expect: str, *, skip: int = 0) -> None: + assert_expected_inline(await dump_github(), expect, skip=skip + 1) def is_direct() -> bool: @@ -447,20 +465,22 @@ def assert_eq(a: Any, b: Any) -> None: assert a == b, f"{a} != {b}" -def assert_raises( +async def assert_raises( exc_type: Type[BaseException], callable: Callable[..., Any], *args: Any, **kwargs: Any, ) -> None: try: - callable(*args, **kwargs) + result = callable(*args, **kwargs) + if inspect.isawaitable(result): + await result except exc_type: return assert False, "did not raise when expected to" -def assert_expected_raises_inline( +async def assert_expected_raises_inline( exc_type: Type[BaseException], callable: Callable[..., Any], expect: str, @@ -468,7 +488,9 @@ def assert_expected_raises_inline( **kwargs: Any, ) -> None: try: - callable(*args, **kwargs) + result = callable(*args, **kwargs) + if inspect.isawaitable(result): + await result except exc_type as e: assert_expected_inline(str(e), expect, skip=1) return diff --git a/src/ghstack/unlink.py b/src/ghstack/unlink.py index 611f2e6..abbee02 100644 --- a/src/ghstack/unlink.py +++ b/src/ghstack/unlink.py @@ -16,7 +16,7 @@ RE_GHSTACK_SOURCE_ID = re.compile(r"^ghstack-source-id: (.+)\n?", re.MULTILINE) -def main( +async def main( *, commits: Optional[List[str]] = None, github: ghstack.github.GitHubEndpoint, @@ -36,30 +36,31 @@ def main( # Use CWD sh = ghstack.shell.Shell() - default_branch = ghstack.github_utils.get_github_repo_info( + repo_info = await ghstack.github_utils.get_github_repo_info( github=github, sh=sh, repo_owner=repo_owner, repo_name=repo_name, github_url=github_url, remote_name=remote_name, - )["default_branch"] + ) + default_branch = repo_info["default_branch"] # Parse the commits parsed_commits: Optional[Set[GitCommitHash]] = None if commits: parsed_commits = set() for c in commits: - parsed_commits.add(GitCommitHash(sh.git("rev-parse", c))) + parsed_commits.add(GitCommitHash(await sh.agit("rev-parse", c))) base = GitCommitHash( - sh.git("merge-base", f"{remote_name}/{default_branch}", "HEAD") + await sh.agit("merge-base", f"{remote_name}/{default_branch}", "HEAD") ) # compute the stack of commits in chronological order (does not # include base) stack = ghstack.git.split_header( - sh.git("rev-list", "--reverse", "--header", "^" + base, "HEAD") + await sh.agit("rev-list", "--reverse", "--header", "^" + base, "HEAD") ) # sanity check the parsed_commits @@ -103,9 +104,9 @@ def main( "-- edited commit_msg:\n{}".format(textwrap.indent(commit_msg, " ")) ) head = GitCommitHash( - sh.git( + await sh.agit( "commit-tree", - *ghstack.gpg_sign.gpg_args_if_necessary(sh), + *(await ghstack.gpg_sign.gpg_args_if_necessary(sh)), s.tree, "-p", head, @@ -113,7 +114,7 @@ def main( ) ) - sh.git("reset", "--soft", head) + await sh.agit("reset", "--soft", head) logging.info( """ diff --git a/test/checkout/basic.py.test b/test/checkout/basic.py.test index 06c5ab9..b6f2aad 100644 --- a/test/checkout/basic.py.test +++ b/test/checkout/basic.py.test @@ -1,24 +1,24 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create a PR to checkout -commit("A") -(A,) = gh_submit("Initial commit") +await commit("A") +(A,) = await gh_submit("Initial commit") # Move to main and create another commit -git("checkout", "main") -commit("B") +await git("checkout", "main") +await commit("B") # Verify we're on main with commit B -current_log = git("log", "--oneline", "-n", "1") +current_log = await git("log", "--oneline", "-n", "1") assert "Commit B" in current_log # Checkout the PR -gh_checkout(f"https://github.com/pytorch/pytorch/pull/{A.number}") +await gh_checkout(f"https://github.com/pytorch/pytorch/pull/{A.number}") # After checkout, we should be on the PR commit -current_log = git("log", "--oneline", "-n", "1") +current_log = await git("log", "--oneline", "-n", "1") assert "Commit A" in current_log ok() diff --git a/test/checkout/same_base_allows.py.test b/test/checkout/same_base_allows.py.test index 2f58ae8..e2c57bb 100644 --- a/test/checkout/same_base_allows.py.test +++ b/test/checkout/same_base_allows.py.test @@ -1,11 +1,11 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create two PRs in a stack - they'll have the same base -commit("A") -commit("B") -diffs = gh_submit("Stack of two commits") +await commit("A") +await commit("B") +diffs = await gh_submit("Stack of two commits") # Should have two PRs assert len(diffs) == 2 @@ -14,18 +14,18 @@ B = diffs[1] # Second commit (B) # Both PRs should have the same merge-base with main (initial commit) # Checkout PR A -gh_checkout(f"https://github.com/pytorch/pytorch/pull/{A.number}") +await gh_checkout(f"https://github.com/pytorch/pytorch/pull/{A.number}") # Verify we're on PR A -current_log = git("log", "--oneline", "-n", "1") +current_log = await git("log", "--oneline", "-n", "1") assert "Commit A" in current_log # Now checkout PR B with --same-base # Since both have the same merge-base (initial commit), this should succeed -gh_checkout(f"https://github.com/pytorch/pytorch/pull/{B.number}", same_base=True) +await gh_checkout(f"https://github.com/pytorch/pytorch/pull/{B.number}", same_base=True) # Verify we successfully checked out PR B -current_log = git("log", "--oneline", "-n", "1") +current_log = await git("log", "--oneline", "-n", "1") assert "Commit B" in current_log ok() diff --git a/test/checkout/same_base_rejects.py.test b/test/checkout/same_base_rejects.py.test index 6ee84ae..7f29077 100644 --- a/test/checkout/same_base_rejects.py.test +++ b/test/checkout/same_base_rejects.py.test @@ -1,35 +1,35 @@ import pytest from ghstack.test_prelude import * -init_test() +await init_test() # Create first PR based on initial main -commit("A") -(A,) = gh_submit("First PR") +await commit("A") +(A,) = await gh_submit("First PR") # Go back to main and advance it -git("checkout", "main") -commit("B") -git("push", "origin", "main") +await git("checkout", "main") +await commit("B") +await git("push", "origin", "main") # Create second PR based on new main (different merge-base) -commit("C") -(C,) = gh_submit("Second PR") +await commit("C") +(C,) = await gh_submit("Second PR") # Checkout first PR -gh_checkout(f"https://github.com/pytorch/pytorch/pull/{A.number}") +await gh_checkout(f"https://github.com/pytorch/pytorch/pull/{A.number}") # Verify we're on PR A -current_log = git("log", "--oneline", "-n", "1") +current_log = await git("log", "--oneline", "-n", "1") assert "Commit A" in current_log # Try to checkout second PR with --same-base # This should fail because merge-base would change from initial commit to commit B with pytest.raises(RuntimeError, match="would change merge-base"): - gh_checkout(f"https://github.com/pytorch/pytorch/pull/{C.number}", same_base=True) + await gh_checkout(f"https://github.com/pytorch/pytorch/pull/{C.number}", same_base=True) # Verify we're still on PR A (checkout was aborted) -current_log = git("log", "--oneline", "-n", "1") +current_log = await git("log", "--oneline", "-n", "1") assert "Commit A" in current_log ok() diff --git a/test/cherry_pick/basic.py.test b/test/cherry_pick/basic.py.test index 26ef530..f1ec884 100644 --- a/test/cherry_pick/basic.py.test +++ b/test/cherry_pick/basic.py.test @@ -1,26 +1,26 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create a PR to cherry-pick from on a separate branch -git("checkout", "-b", "feature") -commit("A") -(A,) = gh_submit("Initial commit") +await git("checkout", "-b", "feature") +await commit("A") +(A,) = await gh_submit("Initial commit") # Switch to main and add another commit -git("checkout", "main") -commit("M") +await git("checkout", "main") +await commit("M") # Before cherry-pick, "Commit A" should NOT be in recent main history -log_before = git("log", "--oneline", "-n", "2") +log_before = await git("log", "--oneline", "-n", "2") assert "Commit A" not in log_before assert "Commit M" in log_before # Now cherry-pick the PR commit -gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{A.number}") +await gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{A.number}") # After cherry-pick, "Commit A" SHOULD be in recent main history -log_after = git("log", "--oneline", "-n", "3") +log_after = await git("log", "--oneline", "-n", "3") assert "Commit A" in log_after assert "Commit M" in log_after diff --git a/test/cherry_pick/conflict.py.test b/test/cherry_pick/conflict.py.test index d983e4c..f85c5f5 100644 --- a/test/cherry_pick/conflict.py.test +++ b/test/cherry_pick/conflict.py.test @@ -1,30 +1,30 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create a commit on a separate branch that will cause a conflict when cherry-picked -git("checkout", "-b", "feature") -commit("A") -(A,) = gh_submit("Initial commit") +await git("checkout", "-b", "feature") +await commit("A") +(A,) = await gh_submit("Initial commit") # Switch to main and modify the same file differently -git("checkout", "main") +await git("checkout", "main") # The commit() function creates .txt with content "A" # Let's create a conflicting change by committing to the same file -write_file_and_add("A.txt", "Different content") -git("commit", "-m", "Conflicting change") +await write_file_and_add("A.txt", "Different content") +await git("commit", "-m", "Conflicting change") # Before cherry-pick, verify "Commit A" is not in main -log_before = git("log", "--oneline", "-n", "2") +log_before = await git("log", "--oneline", "-n", "2") assert "Commit A" not in log_before assert "Conflicting change" in log_before # Attempt to cherry-pick - this should now raise an exception due to conflict -git_status_before = git("status", "--porcelain") +git_status_before = await git("status", "--porcelain") assert git_status_before.strip() == "" # Clean working directory try: - gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{A.number}") + await gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{A.number}") assert False, "Expected cherry-pick to fail with conflict" except RuntimeError as e: # Verify it's a cherry-pick failure diff --git a/test/cherry_pick/stack.py.test b/test/cherry_pick/stack.py.test index e225b63..23bf305 100644 --- a/test/cherry_pick/stack.py.test +++ b/test/cherry_pick/stack.py.test @@ -1,29 +1,29 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create a stack of PRs to cherry-pick on a separate branch -git("checkout", "-b", "feature") -commit("A") -commit("B") -A, B = gh_submit("Initial stack") +await git("checkout", "-b", "feature") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial stack") # Switch to main and add another commit -git("checkout", "main") -commit("M") +await git("checkout", "main") +await commit("M") # Before cherry-pick, "Commit B" should NOT be in recent main history -log_before = git("log", "--oneline", "-n", "2") +log_before = await git("log", "--oneline", "-n", "2") assert "Commit B" not in log_before assert "Commit M" in log_before # For now, just test that single cherry-pick works with a stack PR # The stack functionality requires complex URL parsing that doesn't work # in the test environment's temporary directories -gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{B.number}") +await gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{B.number}") # After cherry-pick, "Commit B" SHOULD be in recent main history -log_output = git("log", "--oneline", "-n", "3") +log_output = await git("log", "--oneline", "-n", "3") assert "Commit B" in log_output assert "Commit M" in log_output diff --git a/test/cherry_pick/stack_manual.py.test b/test/cherry_pick/stack_manual.py.test index aa13d5e..77240cf 100644 --- a/test/cherry_pick/stack_manual.py.test +++ b/test/cherry_pick/stack_manual.py.test @@ -1,31 +1,31 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create a more comprehensive stack test by manually testing the git operations # First, create a stack of commits on a branch -git("checkout", "-b", "feature") -commit("A") -commit("B") -commit("C") +await git("checkout", "-b", "feature") +await commit("A") +await commit("B") +await commit("C") # Create PRs for the stack -A, B, C = gh_submit("Stack of 3 commits") +A, B, C = await gh_submit("Stack of 3 commits") # Go back to main and create a divergent commit -git("checkout", "main") -commit("M") +await git("checkout", "main") +await commit("M") # Before cherry-pick, "Commit C" should NOT be in main history -log_before = git("log", "--oneline", "-n", "2") +log_before = await git("log", "--oneline", "-n", "2") assert "Commit C" not in log_before assert "Commit M" in log_before # Now test cherry-picking the top commit only (not stack) -gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{C.number}") +await gh_cherry_pick(f"https://github.com/pytorch/pytorch/pull/{C.number}") # After cherry-pick, should only have C and M in recent main history, not A and B -log_output = git("log", "--oneline", "-n", "4") +log_output = await git("log", "--oneline", "-n", "4") assert "Commit C" in log_output assert "Commit M" in log_output # A and B should not be in recent history (they weren't cherry-picked) diff --git a/test/github_utils/get_repo_name_with_owner.py.test b/test/github_utils/get_repo_name_with_owner.py.test index 9f5e3ef..65030de 100644 --- a/test/github_utils/get_repo_name_with_owner.py.test +++ b/test/github_utils/get_repo_name_with_owner.py.test @@ -1,71 +1,71 @@ from ghstack.test_prelude import * -init_test() +await init_test() sh = get_sh() -git("remote", "add", "normal", "git@github.com:ezyang/ghstack.git") +await git("remote", "add", "normal", "git@github.com:ezyang/ghstack.git") assert_eq( - ghstack.github_utils.get_github_repo_name_with_owner( + await ghstack.github_utils.get_github_repo_name_with_owner( sh=sh, github_url="github.com", remote_name="normal" ), {"owner": "ezyang", "name": "ghstack"}, ) -git("remote", "add", "with-dot", "git@github.com:ezyang/ghstack.dotted.git") +await git("remote", "add", "with-dot", "git@github.com:ezyang/ghstack.dotted.git") assert_eq( - ghstack.github_utils.get_github_repo_name_with_owner( + await ghstack.github_utils.get_github_repo_name_with_owner( sh=sh, github_url="github.com", remote_name="with-dot" ), {"owner": "ezyang", "name": "ghstack.dotted"}, ) -git("remote", "add", "https", "https://github.com/ezyang/ghstack") +await git("remote", "add", "https", "https://github.com/ezyang/ghstack") assert_eq( - ghstack.github_utils.get_github_repo_name_with_owner( + await ghstack.github_utils.get_github_repo_name_with_owner( sh=sh, github_url="github.com", remote_name="https" ), {"owner": "ezyang", "name": "ghstack"}, ) -git( +await git( "remote", "add", "https-with-dotgit", "https://github.com/ezyang/ghstack.git", ) assert_eq( - ghstack.github_utils.get_github_repo_name_with_owner( + await ghstack.github_utils.get_github_repo_name_with_owner( sh=sh, github_url="github.com", remote_name="https-with-dotgit" ), {"owner": "ezyang", "name": "ghstack"}, ) -git( +await git( "remote", "add", "https-with-dot", "https://github.com/ezyang/ghstack.dotted", ) assert_eq( - ghstack.github_utils.get_github_repo_name_with_owner( + await ghstack.github_utils.get_github_repo_name_with_owner( sh=sh, github_url="github.com", remote_name="https-with-dot" ), {"owner": "ezyang", "name": "ghstack.dotted"}, ) -git( +await git( "remote", "add", "https-with-dot-with-dotgit", "https://github.com/ezyang/ghstack.dotted.git", ) assert_eq( - ghstack.github_utils.get_github_repo_name_with_owner( + await ghstack.github_utils.get_github_repo_name_with_owner( sh=sh, github_url="github.com", remote_name="https-with-dot-with-dotgit", ), {"owner": "ezyang", "name": "ghstack.dotted"}, ) -git("remote", "add", "with-leading-slash", "git@github.com:/ezyang/ghstack.git") +await git("remote", "add", "with-leading-slash", "git@github.com:/ezyang/ghstack.git") assert_eq( - ghstack.github_utils.get_github_repo_name_with_owner( + await ghstack.github_utils.get_github_repo_name_with_owner( sh=sh, github_url="github.com", remote_name="with-leading-slash" ), {"owner": "ezyang", "name": "ghstack"}, diff --git a/test/land/default_branch_change.py.test b/test/land/default_branch_change.py.test index 9bb5216..60d7b46 100644 --- a/test/land/default_branch_change.py.test +++ b/test/land/default_branch_change.py.test @@ -1,30 +1,30 @@ import os from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(diff1,) = gh_submit("Initial 1") +await commit("A") +(diff1,) = await gh_submit("Initial 1") assert diff1 is not None # make release branch -git("branch", "release", "main") -git("push", "origin", "release") +await git("branch", "release", "main") +await git("push", "origin", "release") # change default branch to release -get_github().patch( +await get_github().apatch( "repos/pytorch/pytorch", name="pytorch", default_branch="release", ) # invalidate repo info cache since default branch changed cache_path = os.path.join( - get_sh().abspath(get_sh().git("rev-parse", "--git-dir")), + get_sh().abspath(await get_sh().agit("rev-parse", "--git-dir")), "ghstack-repo-info.json", ) if os.path.exists(cache_path): os.remove(cache_path) -assert_github_state( +await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) @@ -47,26 +47,26 @@ assert_github_state( ) # land -gh_land(diff1.pr_url) +await gh_land(diff1.pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + await get_upstream_sh().agit("log", "--oneline", "main"), """dc8bfe4 Initial commit""", ) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "release"), + await get_upstream_sh().agit("log", "--oneline", "release"), """\ 8927014 Commit A dc8bfe4 Initial commit""", ) # make another commit -commit("B") -(diff2,) = gh_submit("Initial 2") +await commit("B") +(diff2,) = await gh_submit("Initial 2") assert diff2 is not None # change default branch back to main -get_github().patch( +await get_github().apatch( "repos/pytorch/pytorch", name="pytorch", default_branch="main", @@ -74,7 +74,7 @@ get_github().patch( if os.path.exists(cache_path): os.remove(cache_path) -assert_github_state( +await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) @@ -112,17 +112,17 @@ assert_github_state( ) # land again -gh_land(diff2.pr_url) +await gh_land(diff2.pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + await get_upstream_sh().agit("log", "--oneline", "main"), """\ 6b7e56e Commit B (#501) 1132c50 Commit A (#500) dc8bfe4 Initial commit""", ) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "release"), + await get_upstream_sh().agit("log", "--oneline", "release"), """\ 8927014 Commit A dc8bfe4 Initial commit""", diff --git a/test/land/early_mod.py.test b/test/land/early_mod.py.test index 089f0e1..780d707 100644 --- a/test/land/early_mod.py.test +++ b/test/land/early_mod.py.test @@ -1,22 +1,22 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") +await commit("A") +await commit("B") ( diff1, diff2, -) = gh_submit("Initial") +) = await gh_submit("Initial") assert diff1 is not None assert diff2 is not None pr_url = diff2.pr_url # edit earlier commit -git("checkout", "HEAD~") -amend("A2") -gh_submit("Update") +await git("checkout", "HEAD~") +await amend("A2") +await gh_submit("Update") -gh_land(pr_url) -assert_expected_inline(get_upstream_sh().git("show", "main:A2.txt"), """A""") -assert_expected_inline(get_upstream_sh().git("show", "main:B.txt"), """A""") +await gh_land(pr_url) +assert_expected_inline(await get_upstream_sh().agit("show", "main:A2.txt"), """A""") +assert_expected_inline(await get_upstream_sh().agit("show", "main:B.txt"), """A""") diff --git a/test/land/ff.py.test b/test/land/ff.py.test index 4c771b1..eb48d7f 100644 --- a/test/land/ff.py.test +++ b/test/land/ff.py.test @@ -1,15 +1,15 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(diff,) = gh_submit("Initial") +await commit("A") +(diff,) = await gh_submit("Initial") assert diff is not None pr_url = diff.pr_url -gh_land(pr_url) +await gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + await get_upstream_sh().agit("log", "--oneline", "main"), """\ d518c9f Commit A (#500) dc8bfe4 Initial commit""", diff --git a/test/land/ff_stack.py.test b/test/land/ff_stack.py.test index e40f11e..9d283dc 100644 --- a/test/land/ff_stack.py.test +++ b/test/land/ff_stack.py.test @@ -1,20 +1,20 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") +await commit("A") +await commit("B") ( diff1, diff2, -) = gh_submit("Initial") +) = await gh_submit("Initial") assert diff1 is not None assert diff2 is not None pr_url = diff2.pr_url -gh_land(pr_url) +await gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + await get_upstream_sh().agit("log", "--oneline", "main"), """\ 4099517 Commit B (#501) c28edd5 Commit A (#500) diff --git a/test/land/ff_stack_two_phase.py.test b/test/land/ff_stack_two_phase.py.test index d2c0ca6..f074d7d 100644 --- a/test/land/ff_stack_two_phase.py.test +++ b/test/land/ff_stack_two_phase.py.test @@ -1,22 +1,22 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") +await commit("A") +await commit("B") ( diff1, diff2, -) = gh_submit("Initial") +) = await gh_submit("Initial") assert diff1 is not None assert diff2 is not None pr_url1 = diff1.pr_url pr_url2 = diff2.pr_url -gh_land(pr_url1) -gh_land(pr_url2) +await gh_land(pr_url1) +await gh_land(pr_url2) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + await get_upstream_sh().agit("log", "--oneline", "main"), """\ 4099517 Commit B (#501) c28edd5 Commit A (#500) diff --git a/test/land/invalid_resubmit.py.test b/test/land/invalid_resubmit.py.test index af7a06c..2c7d97e 100644 --- a/test/land/invalid_resubmit.py.test +++ b/test/land/invalid_resubmit.py.test @@ -1,32 +1,32 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(diff,) = gh_submit("Initial") +await commit("A") +(diff,) = await gh_submit("Initial") assert diff is not None pr_url = diff.pr_url -gh_land(pr_url) +await gh_land(pr_url) -write_file_and_add("file2.txt", "A") -git("commit", "--amend", "--no-edit") -assert_expected_raises_inline( +await write_file_and_add("file2.txt", "A") +await git("commit", "--amend", "--no-edit") +await assert_expected_raises_inline( RuntimeError, lambda: gh_submit("Update"), """Cannot ghstack a stack with closed PR #500 whose branch was deleted. If you were just trying to update a later PR in the stack, `git rebase` and try again. Otherwise, you may have been trying to update a PR that was already closed. To disassociate your update from the old PR and open a new PR, run `ghstack unlink`, `git rebase` and then try again.""", ) # Do the remediation -gh_unlink() -git("rebase", "origin/main") -gh_submit("New PR") +await gh_unlink() +await git("rebase", "origin/main") +await gh_submit("New PR") if is_direct(): - assert_github_state("""""") + await assert_github_state("""""") else: - assert_github_state( + await assert_github_state( """\ [X] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) @@ -63,6 +63,6 @@ ok() # only the amend shows up now assert_expected_inline( - git("show", "--pretty=", "--name-only", "origin/gh/ezyang/1/orig"), + await git("show", "--pretty=", "--name-only", "origin/gh/ezyang/1/orig"), """file2.txt""", ) diff --git a/test/land/non_ff.py.test b/test/land/non_ff.py.test index 4493f4a..42f9238 100644 --- a/test/land/non_ff.py.test +++ b/test/land/non_ff.py.test @@ -1,21 +1,21 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(diff,) = gh_submit("Initial") +await commit("A") +(diff,) = await gh_submit("Initial") assert diff is not None pr_url = diff.pr_url -git("reset", "--hard", "origin/main") -commit("U") -git("push") +await git("reset", "--hard", "origin/main") +await commit("U") +await git("push") -git("checkout", "gh/ezyang/1/orig") -gh_land(pr_url) +await git("checkout", "gh/ezyang/1/orig") +await gh_land(pr_url) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + await get_upstream_sh().agit("log", "--oneline", "main"), """\ 8b61aeb Commit A (#500) 38808c0 Commit U diff --git a/test/land/non_ff_stack_two_phase.py.test b/test/land/non_ff_stack_two_phase.py.test index 36e44dc..4f46ad3 100644 --- a/test/land/non_ff_stack_two_phase.py.test +++ b/test/land/non_ff_stack_two_phase.py.test @@ -1,26 +1,26 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") +await commit("A") +await commit("B") ( diff1, diff2, -) = gh_submit("Initial") +) = await gh_submit("Initial") assert diff1 is not None assert diff2 is not None pr_url1 = diff1.pr_url pr_url2 = diff2.pr_url -git("checkout", "origin/main") -commit("C") -git("push", "origin", "HEAD:main") +await git("checkout", "origin/main") +await commit("C") +await git("push", "origin", "HEAD:main") -gh_land(pr_url1) -gh_land(pr_url2) +await gh_land(pr_url1) +await gh_land(pr_url2) assert_expected_inline( - get_upstream_sh().git("log", "--oneline", "main"), + await get_upstream_sh().agit("log", "--oneline", "main"), """\ 402e96c Commit B (#501) e388a10 Commit A (#500) diff --git a/test/land/reuse_branch_refuse_land.py.test b/test/land/reuse_branch_refuse_land.py.test index 14dac30..e009742 100644 --- a/test/land/reuse_branch_refuse_land.py.test +++ b/test/land/reuse_branch_refuse_land.py.test @@ -1,25 +1,25 @@ from ghstack.test_prelude import * -init_test() +await init_test() # make a stack -commit("A") -(diff1,) = gh_submit("Initial 1") +await commit("A") +(diff1,) = await gh_submit("Initial 1") assert diff1 is not None # land first pr -gh_land(diff1.pr_url) +await gh_land(diff1.pr_url) # make another stack -commit("B") -(diff2,) = gh_submit("Second 2") +await commit("B") +(diff2,) = await gh_submit("Second 2") assert diff2 is not None # check the head number was reused assert_eq(diff1.ghnum, diff2.ghnum) # refuse to reland first pr -assert_expected_raises_inline( +await assert_expected_raises_inline( RuntimeError, lambda: gh_land(diff1.pr_url), """PR is already closed, cannot land it!""", diff --git a/test/land/update_after_land.py.test b/test/land/update_after_land.py.test index 5e11c51..ab88ae8 100644 --- a/test/land/update_after_land.py.test +++ b/test/land/update_after_land.py.test @@ -1,42 +1,42 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -(diff1, diff2) = gh_submit("Initial 1") +await commit("A") +await commit("B") +(diff1, diff2) = await gh_submit("Initial 1") assert diff1 is not None assert diff2 is not None # setup an upstream commit, so the land isn't just trivial -git("reset", "--hard", "origin/main") -commit("U") -git("push") +await git("reset", "--hard", "origin/main") +await commit("U") +await git("push") # land first pr -gh_land(diff1.pr_url) +await gh_land(diff1.pr_url) # go back to stack -git("checkout", "gh/ezyang/2/orig") +await git("checkout", "gh/ezyang/2/orig") # update second pr -amend("B2") +await amend("B2") # try to push -assert_expected_raises_inline( +await assert_expected_raises_inline( RuntimeError, lambda: gh_submit("Run 2"), """Cannot ghstack a stack with closed PR #500 whose branch was deleted. If you were just trying to update a later PR in the stack, `git rebase` and try again. Otherwise, you may have been trying to update a PR that was already closed. To disassociate your update from the old PR and open a new PR, run `ghstack unlink`, `git rebase` and then try again.""", ) # show the remediation works -git("rebase", "origin/main") -gh_submit("Run 3") +await git("rebase", "origin/main") +await gh_submit("Run 3") if is_direct(): - assert_github_state("""""") + await assert_github_state("""""") else: - assert_github_state( + await assert_github_state( """\ [X] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/log/basic.py.test b/test/log/basic.py.test index 96606e4..d34554a 100644 --- a/test/log/basic.py.test +++ b/test/log/basic.py.test @@ -1,24 +1,24 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create a PR with multiple updates so the head branch has multiple commits -commit("A") -gh_submit("Initial commit") +await commit("A") +await gh_submit("Initial commit") -write_file_and_add("A.txt", "A2") -git("commit", "--amend", "--no-edit", tick=True) -gh_submit("Second update") +await write_file_and_add("A.txt", "A2") +await git("commit", "--amend", "--no-edit", tick=True) +await gh_submit("Second update") -write_file_and_add("A.txt", "A3") -git("commit", "--amend", "--no-edit", tick=True) -gh_submit("Third update") +await write_file_and_add("A.txt", "A3") +await git("commit", "--amend", "--no-edit", tick=True) +await gh_submit("Third update") # HEAD now has the Pull-Request trailer from the most recent submit; # gh_log should figure out the PR on its own. Use --format=%s to make # the assertion stable against commit-hash churn. with captured_output() as (out, _): - gh_log(args=["--format=%s"]) + await gh_log(args=["--format=%s"]) assert_expected_inline( out.getvalue(), """\ diff --git a/test/log/explicit_pr.py.test b/test/log/explicit_pr.py.test index b7e10dc..2a17701 100644 --- a/test/log/explicit_pr.py.test +++ b/test/log/explicit_pr.py.test @@ -1,16 +1,16 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Submit a PR, then switch HEAD away. Explicit-PR mode should still work # and should NOT include any pending-changes commit (local HEAD is irrelevant). -commit("A") -(A,) = gh_submit("Initial commit") +await commit("A") +(A,) = await gh_submit("Initial commit") -git("checkout", "main") +await git("checkout", "main") with captured_output() as (out, _): - gh_log( + await gh_log( pull_request=f"https://github.com/pytorch/pytorch/pull/{A.number}", args=["--format=%s"], ) diff --git a/test/log/pending_diff.py.test b/test/log/pending_diff.py.test index 601eff3..5a1b6fb 100644 --- a/test/log/pending_diff.py.test +++ b/test/log/pending_diff.py.test @@ -1,19 +1,19 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create a PR, then make local changes that haven't been pushed yet. # ghstack log should show those local-vs-remote changes as a synthesized # "Local pending changes" commit at the top. -commit("A") -gh_submit("Initial commit") +await commit("A") +await gh_submit("Initial commit") # Make a local change but don't submit -write_file_and_add("A.txt", "pending local change") -git("commit", "--amend", "--no-edit", tick=True) +await write_file_and_add("A.txt", "pending local change") +await git("commit", "--amend", "--no-edit", tick=True) with captured_output() as (out, _): - gh_log(args=["--format=%s"]) + await gh_log(args=["--format=%s"]) assert_expected_inline( out.getvalue(), """\ @@ -24,7 +24,7 @@ Initial commit # With -p, the synthesized commit's diff shows the pending content. with captured_output() as (out, _): - gh_log(args=["-p", "--format=%s"]) + await gh_log(args=["-p", "--format=%s"]) assert_expected_inline( out.getvalue(), """\ diff --git a/test/log/stack.py.test b/test/log/stack.py.test index 524f110..182cb94 100644 --- a/test/log/stack.py.test +++ b/test/log/stack.py.test @@ -1,19 +1,19 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Build a 2-PR stack, then update the top PR once -commit("A") -commit("B") -A, B = gh_submit("Initial") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial") -write_file_and_add("B.txt", "B2") -git("commit", "--amend", "--no-edit", tick=True) -gh_submit("Top update") +await write_file_and_add("B.txt", "B2") +await git("commit", "--amend", "--no-edit", tick=True) +await gh_submit("Top update") # HEAD is the top PR (B) -- gh_log picks it up from HEAD's trailer. with captured_output() as (out, _): - gh_log(args=["--format=%s"]) + await gh_log(args=["--format=%s"]) assert_expected_inline( out.getvalue(), """\ @@ -23,9 +23,9 @@ Initial ) # Check out the bottom PR locally; HEAD's trailer now points to PR A. -checkout(A) +await checkout(A) with captured_output() as (out, _): - gh_log(args=["--format=%s"]) + await gh_log(args=["--format=%s"]) assert_expected_inline( out.getvalue(), """\ diff --git a/test/submit/amend.py.test b/test/submit/amend.py.test index 87b359b..4f52ccd 100644 --- a/test/submit/amend.py.test +++ b/test/submit/amend.py.test @@ -1,15 +1,15 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(A,) = gh_submit("Initial 1") +await commit("A") +(A,) = await gh_submit("Initial 1") -amend("A2") -(A2,) = gh_submit("Update A") +await amend("A2") +(A2,) = await gh_submit("Update A") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -29,7 +29,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/amend_all.py.test b/test/submit/amend_all.py.test index f37f8bc..0817c01 100644 --- a/test/submit/amend_all.py.test +++ b/test/submit/amend_all.py.test @@ -1,21 +1,21 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -gh_submit("Initial 1") +await commit("A") +await gh_submit("Initial 1") -commit("B") -A2, B2 = gh_submit("Initial 2") +await commit("B") +A2, B2 = await gh_submit("Initial 2") -checkout(A2) -amend("A3") -cherry_pick(B2) -amend("B3") -A3, B3 = gh_submit("Update A") +await checkout(A2) +await amend("A3") +await cherry_pick(B2) +await amend("B3") +A3, B3 = await gh_submit("Update A") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -46,7 +46,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/amend_bottom.py.test b/test/submit/amend_bottom.py.test index 0e08154..1ecba1e 100644 --- a/test/submit/amend_bottom.py.test +++ b/test/submit/amend_bottom.py.test @@ -1,21 +1,21 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(A,) = gh_submit("Initial 1") -commit("B") -A2, B2 = gh_submit("Initial 2") +await commit("A") +(A,) = await gh_submit("Initial 1") +await commit("B") +A2, B2 = await gh_submit("Initial 2") -checkout(A2) -amend("A3") -(A3,) = gh_submit("Update A") +await checkout(A2) +await amend("A3") +(A3,) = await gh_submit("Update A") -cherry_pick(B2) -A4, B4 = gh_submit("Update B") +await cherry_pick(B2) +A4, B4 = await gh_submit("Update B") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -46,7 +46,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/amend_message_only.py.test b/test/submit/amend_message_only.py.test index d7ab89b..5eeb666 100644 --- a/test/submit/amend_message_only.py.test +++ b/test/submit/amend_message_only.py.test @@ -1,16 +1,16 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("AAA") -(A,) = gh_submit("Initial 1") +await commit("AAA") +(A,) = await gh_submit("Initial 1") assert "AAA" in A.commit_msg -git("commit", "--amend", "-m", A.commit_msg.replace("AAA", "BBB")) +await git("commit", "--amend", "-m", A.commit_msg.replace("AAA", "BBB")) -(A2,) = gh_submit("Update A", no_skip=True) +(A2,) = await gh_submit("Update A", no_skip=True) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit AAA (gh/ezyang/1/head -> main) @@ -27,7 +27,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit AAA (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/amend_out_of_date.py.test b/test/submit/amend_out_of_date.py.test index 5147e12..2dc090c 100644 --- a/test/submit/amend_out_of_date.py.test +++ b/test/submit/amend_out_of_date.py.test @@ -1,17 +1,17 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(A,) = gh_submit("Initial 1") +await commit("A") +(A,) = await gh_submit("Initial 1") -amend("A2") -(A2,) = gh_submit("Update A") +await amend("A2") +(A2,) = await gh_submit("Update A") # Reset to the old version -git("reset", "--hard", A.orig) -amend("A3") -assert_expected_raises_inline( +await git("reset", "--hard", A.orig) +await amend("A3") +await assert_expected_raises_inline( RuntimeError, lambda: gh_submit("Update B"), """Cowardly refusing to push an update to GitHub, since it looks another source has updated GitHub since you last pushed. If you want to push anyway, rerun this command with --force. Otherwise, diff your changes against cbc98e2976fd71cb5552f4acef54d93c300a3827 and reapply them on top of an up-to-date commit from GitHub.""", diff --git a/test/submit/amend_top.py.test b/test/submit/amend_top.py.test index 1148d6a..b79b719 100644 --- a/test/submit/amend_top.py.test +++ b/test/submit/amend_top.py.test @@ -1,18 +1,18 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(A,) = gh_submit("Initial 1") +await commit("A") +(A,) = await gh_submit("Initial 1") -commit("B") -A2, B2 = gh_submit("Initial 2") +await commit("B") +A2, B2 = await gh_submit("Initial 2") -amend("B2") -A3, B3 = gh_submit("Update A") +await amend("B2") +A3, B3 = await gh_submit("Update A") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -40,7 +40,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/bullet_divider.py.test b/test/submit/bullet_divider.py.test index 01dac4d..5d6c60e 100644 --- a/test/submit/bullet_divider.py.test +++ b/test/submit/bullet_divider.py.test @@ -1,9 +1,9 @@ from ghstack.test_prelude import * -init_test() +await init_test() -write_file_and_add("file1.txt", "A") -git( +await write_file_and_add("file1.txt", "A") +await git( "commit", "-m", """This is my commit @@ -12,10 +12,10 @@ git( * Bullet list""", ) tick() -gh_submit("Initial") +await gh_submit("Initial") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 This is my commit (gh/ezyang/1/head -> main) @@ -33,7 +33,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 This is my commit (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/cherry_pick.py.test b/test/submit/cherry_pick.py.test index 5899455..ae5f83a 100644 --- a/test/submit/cherry_pick.py.test +++ b/test/submit/cherry_pick.py.test @@ -1,22 +1,22 @@ from ghstack.test_prelude import * -init_test() +await init_test() -git("checkout", "-b", "feature") +await git("checkout", "-b", "feature") -commit("A") -commit("B") -A, B = gh_submit("Initial 2") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial 2") -git("checkout", "main") -commit("M") -git("push", "origin", "main") +await git("checkout", "main") +await commit("M") +await git("push", "origin", "main") -cherry_pick(B) -gh_submit("Cherry pick") +await cherry_pick(B) +await gh_submit("Cherry pick") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -47,7 +47,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/cli_reviewer_and_label.py.test b/test/submit/cli_reviewer_and_label.py.test index 2a4757d..9e1debe 100644 --- a/test/submit/cli_reviewer_and_label.py.test +++ b/test/submit/cli_reviewer_and_label.py.test @@ -1,18 +1,18 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Create first commit with one set of reviewers/labels -commit("A") -(A,) = gh_submit("Initial commit", reviewer="reviewer1", label="bug") +await commit("A") +(A,) = await gh_submit("Initial commit", reviewer="reviewer1", label="bug") # Verify first PR has correct reviewers and labels assert_eq(get_pr_reviewers(500), ["reviewer1"]) assert_eq(get_pr_labels(500), ["bug"]) # Create second commit with different reviewers/labels -commit("B") -(A2, B) = gh_submit( +await commit("B") +(A2, B) = await gh_submit( "Add B", reviewer="reviewer2,reviewer3", label="enhancement,priority-high" ) @@ -21,7 +21,7 @@ assert_eq(get_pr_reviewers(501), ["reviewer2", "reviewer3"]) assert_eq(get_pr_labels(501), ["enhancement", "priority-high"]) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -46,7 +46,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/commit_amended_to_empty.py.test b/test/submit/commit_amended_to_empty.py.test index 06356bd..1049ca1 100644 --- a/test/submit/commit_amended_to_empty.py.test +++ b/test/submit/commit_amended_to_empty.py.test @@ -1,21 +1,21 @@ from ghstack.test_prelude import * -init_test() +await init_test() -write_file_and_add("bar", "baz") -git("commit", "-m", "Commit 1\n\nThis is my first commit") +await write_file_and_add("bar", "baz") +await git("commit", "-m", "Commit 1\n\nThis is my first commit") -(A,) = gh_submit("Initial") +(A,) = await gh_submit("Initial") -git("rm", "bar") -git("commit", "--amend", "--allow-empty", "--no-edit") +await git("rm", "bar") +await git("commit", "--amend", "--allow-empty", "--no-edit") tick() # TODO: direct NYI if not is_direct(): - gh_submit("Update") + await gh_submit("Update") - assert_github_state( + await assert_github_state( """\ [O] #500 Commit 1 (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/do_not_revert_local_commit_msg_on_skip.py.test b/test/submit/do_not_revert_local_commit_msg_on_skip.py.test index a292a8d..df268f7 100644 --- a/test/submit/do_not_revert_local_commit_msg_on_skip.py.test +++ b/test/submit/do_not_revert_local_commit_msg_on_skip.py.test @@ -1,15 +1,15 @@ from ghstack.test_prelude import * import textwrap -init_test() +await init_test() -commit("TO_REPLACE") -(A,) = gh_submit("Initial") -git("commit", "--amend", "-m", A.commit_msg.replace("TO_REPLACE", "ARGLE")) -(A2,) = gh_submit("Skip") +await commit("TO_REPLACE") +(A,) = await gh_submit("Initial") +await git("commit", "--amend", "-m", A.commit_msg.replace("TO_REPLACE", "ARGLE")) +(A2,) = await gh_submit("Skip") if is_direct(): assert_expected_inline( - textwrap.indent(git("show", "-s", "--pretty=%B", "HEAD"), " " * 8), + textwrap.indent(await git("show", "-s", "--pretty=%B", "HEAD"), " " * 8), """\ Commit ARGLE @@ -21,7 +21,7 @@ if is_direct(): ) else: assert_expected_inline( - textwrap.indent(git("show", "-s", "--pretty=%B", "HEAD"), " " * 8), + textwrap.indent(await git("show", "-s", "--pretty=%B", "HEAD"), " " * 8), """\ Commit ARGLE @@ -32,7 +32,7 @@ else: ) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit TO_REPLACE (gh/ezyang/1/head -> main) @@ -49,7 +49,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit TO_REPLACE (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/empty_commit.py.test b/test/submit/empty_commit.py.test index 8ce39d0..ee86f84 100644 --- a/test/submit/empty_commit.py.test +++ b/test/submit/empty_commit.py.test @@ -1,14 +1,14 @@ from ghstack.test_prelude import * -git("commit", "--allow-empty", "-m", "Commit 1\n\nThis is my first commit") -commit("B") +await git("commit", "--allow-empty", "-m", "Commit 1\n\nThis is my first commit") +await commit("B") if is_direct(): # TODO: NYI pass else: - gh_submit("Initial") - assert_github_state( + await gh_submit("Initial") + await assert_github_state( """\ [O] #500 Commit B (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/fail_same_source_id.py.test b/test/submit/fail_same_source_id.py.test index 91c7eb7..8017d17 100644 --- a/test/submit/fail_same_source_id.py.test +++ b/test/submit/fail_same_source_id.py.test @@ -1,15 +1,15 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -gh_submit("Initial") +await commit("A") +await gh_submit("Initial") # botch it up -write_file_and_add("file2.txt", "A") -git("commit", "-C", "HEAD") +await write_file_and_add("file2.txt", "A") +await git("commit", "-C", "HEAD") tick() -assert_expected_raises_inline( +await assert_expected_raises_inline( RuntimeError, lambda: gh_submit("Should fail"), """Something very strange has happened: a commit for the gh/ezyang/1 occurs twice in your local commit stack. This is usually because of a botched rebase. Please take a look at your git log and seek help from your local Git expert.""", diff --git a/test/submit/minimal_fetch.py.test b/test/submit/minimal_fetch.py.test index 87cad92..9f817fc 100644 --- a/test/submit/minimal_fetch.py.test +++ b/test/submit/minimal_fetch.py.test @@ -1,18 +1,18 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Narrow down the fetch on origin -git( +await git( "config", "remote.origin.fetch", "+refs/heads/main:refs/remotes/origin/main", ) -commit("A") -gh_submit("Initial 1") +await commit("A") +await gh_submit("Initial 1") -amend("A2") -gh_submit("Update 2") +await amend("A2") +await gh_submit("Update 2") ok() diff --git a/test/submit/multi.py.test b/test/submit/multi.py.test index 1cce0cc..fd5950e 100644 --- a/test/submit/multi.py.test +++ b/test/submit/multi.py.test @@ -1,11 +1,11 @@ from ghstack.test_prelude import * -commit("A") -commit("B") -A, B = gh_submit("Initial 1 and 2") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial 1 and 2") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -30,7 +30,7 @@ if is_direct(): """, ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/no_clobber.py.test b/test/submit/no_clobber.py.test index 735751d..ad31eb7 100644 --- a/test/submit/no_clobber.py.test +++ b/test/submit/no_clobber.py.test @@ -1,16 +1,16 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Check that we don't clobber changes to PR description or title -write_file_and_add("b", "asdf") -git("commit", "-m", "Commit 1\n\nOriginal message") +await write_file_and_add("b", "asdf") +await git("commit", "-m", "Commit 1\n\nOriginal message") tick() -gh_submit("Initial 1") +await gh_submit("Initial 1") tick() -get_github().patch( +await get_github().apatch( "repos/pytorch/pytorch/pulls/500", body="""\ Stack: @@ -21,7 +21,7 @@ Directly updated message body""", ) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> main) @@ -41,7 +41,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> gh/ezyang/1/base) @@ -63,14 +63,14 @@ else: """ ) -write_file_and_add("file1.txt", "A") -git("commit", "--amend", "--no-edit") +await write_file_and_add("file1.txt", "A") +await git("commit", "--amend", "--no-edit") tick() -gh_submit("Update 1") +await gh_submit("Update 1") tick() if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> main) @@ -93,7 +93,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/no_clobber_carriage_returns.py.test b/test/submit/no_clobber_carriage_returns.py.test index 824c562..2129218 100644 --- a/test/submit/no_clobber_carriage_returns.py.test +++ b/test/submit/no_clobber_carriage_returns.py.test @@ -1,18 +1,18 @@ from ghstack.test_prelude import * -init_test() +await init_test() # In some situations, GitHub will replace your newlines with # \r\n. Check we handle this correctly. -write_file_and_add("b", "asdf") -git("commit", "-m", "Commit 1\n\nOriginal message") +await write_file_and_add("b", "asdf") +await git("commit", "-m", "Commit 1\n\nOriginal message") tick() -gh_submit("Initial 1") +await gh_submit("Initial 1") tick() if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit 1 (gh/ezyang/1/head -> main) @@ -29,7 +29,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit 1 (gh/ezyang/1/head -> gh/ezyang/1/base) @@ -51,7 +51,7 @@ else: """ ) -get_github().patch( +await get_github().apatch( "repos/pytorch/pytorch/pulls/500", body="""\ Stack: @@ -63,14 +63,14 @@ Directly updated message body""".replace( title="Directly updated title", ) -write_file_and_add("file1.txt", "A") -git("commit", "-m", "Commit 2") +await write_file_and_add("file1.txt", "A") +await git("commit", "-m", "Commit 2") tick() -gh_submit("Initial 2") +await gh_submit("Initial 2") tick() if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> main) @@ -99,7 +99,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/non_standard_base.py.test b/test/submit/non_standard_base.py.test index a0f5820..14a1fa0 100644 --- a/test/submit/non_standard_base.py.test +++ b/test/submit/non_standard_base.py.test @@ -1,27 +1,27 @@ from ghstack.test_prelude import * -init_test() +await init_test() # make release branch -git("branch", "release", "main") +await git("branch", "release", "main") # diverge release and regular branch -git("checkout", "main") -commit("M") -git("push", "origin", "main") +await git("checkout", "main") +await commit("M") +await git("push", "origin", "main") -git("checkout", "release") -commit("R") -git("push", "origin", "release") +await git("checkout", "release") +await commit("R") +await git("push", "origin", "release") # make commit on release branch -commit("A") +await commit("A") # use non-standard base -gh_submit("Initial 1", base="release") +await gh_submit("Initial 1", base="release") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> release) @@ -40,7 +40,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/prefix_only_no_stack.py.test b/test/submit/prefix_only_no_stack.py.test index a6e6280..643d8a7 100644 --- a/test/submit/prefix_only_no_stack.py.test +++ b/test/submit/prefix_only_no_stack.py.test @@ -1,14 +1,14 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -A, B = gh_submit("Initial") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial") -checkout(A) -amend("A2") -cherry_pick(B) -(A2,) = gh_submit("Update base only", revs=["HEAD~"], stack=False) +await checkout(A) +await amend("A2") +await cherry_pick(B) +(A2,) = await gh_submit("Update base only", revs=["HEAD~"], stack=False) assert_eq(A.number, A2.number) diff --git a/test/submit/prefix_only_stack.py.test b/test/submit/prefix_only_stack.py.test index 034c967..cbbfdec 100644 --- a/test/submit/prefix_only_stack.py.test +++ b/test/submit/prefix_only_stack.py.test @@ -1,17 +1,17 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -commit("C") -A, B, C = gh_submit("Initial") +await commit("A") +await commit("B") +await commit("C") +A, B, C = await gh_submit("Initial") -checkout(A) -amend("A2") -cherry_pick(B) -cherry_pick(C) -A2, B2 = gh_submit("Don't update C", revs=["HEAD~"], stack=True) +await checkout(A) +await amend("A2") +await cherry_pick(B) +await cherry_pick(C) +A2, B2 = await gh_submit("Don't update C", revs=["HEAD~"], stack=True) assert_eq(A.number, A2.number) assert_eq(B.number, B2.number) diff --git a/test/submit/preserve_authorship.py.test b/test/submit/preserve_authorship.py.test index 4bf6be3..02125b2 100644 --- a/test/submit/preserve_authorship.py.test +++ b/test/submit/preserve_authorship.py.test @@ -1,10 +1,10 @@ from ghstack.test_prelude import * -init_test() +await init_test() # make a commit with non-standard author -write_file_and_add("file1.txt", "A") -git( +await write_file_and_add("file1.txt", "A") +await git( "commit", "-m", "Commit 1\n\nThis is my first commit", @@ -16,10 +16,10 @@ git( tick() # ghstack -(diff1,) = gh_submit("Initial 1") +(diff1,) = await gh_submit("Initial 1") assert diff1 is not None assert_expected_inline( - git( + await git( "log", "--format=Author: %an <%ae>\nCommitter: %cn <%ce>", "-n1", diff --git a/test/submit/preserve_downstream_closed.py.test b/test/submit/preserve_downstream_closed.py.test index b96c6f0..1064eaa 100644 --- a/test/submit/preserve_downstream_closed.py.test +++ b/test/submit/preserve_downstream_closed.py.test @@ -1,11 +1,11 @@ from ghstack.test_prelude import * from ghstack.github_fake import GitHubNumber -init_test() +await init_test() -commit("A") -commit("B") -A, B = gh_submit("Initial") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial") # Close B directly (simulating a landed/closed PR) github = get_github() @@ -14,19 +14,19 @@ pr_b = github.state.pull_request(repo, GitHubNumber(B.number)) pr_b.closed = True # Go back to just A (don't cherry-pick B since it's closed) -checkout(A) -amend("A2") +await checkout(A) +await amend("A2") # Submit only A. Old stack text mentions B but B is closed, so # it should NOT appear as an orphan. -(A2,) = gh_submit("Update base only") +(A2,) = await gh_submit("Update base only") assert_eq(A.number, A2.number) if is_direct(): pass else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/preserve_downstream_middle.py.test b/test/submit/preserve_downstream_middle.py.test index 173ade9..1be4d3b 100644 --- a/test/submit/preserve_downstream_middle.py.test +++ b/test/submit/preserve_downstream_middle.py.test @@ -1,19 +1,19 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -commit("C") -A, B, C = gh_submit("Initial") +await commit("A") +await commit("B") +await commit("C") +A, B, C = await gh_submit("Initial") # Go back to B, amend it, cherry-pick C on top -checkout(B) -amend("B2") -cherry_pick(C) +await checkout(B) +await amend("B2") +await cherry_pick(C) # Submit only B (the middle), not A or C -(B2,) = gh_submit("Update middle only", revs=["HEAD~"], stack=False) +(B2,) = await gh_submit("Update middle only", revs=["HEAD~"], stack=False) assert_eq(B.number, B2.number) @@ -21,7 +21,7 @@ if is_direct(): pass else: # We expect A (#500) below and C (#502) above to both appear - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/preserve_downstream_multiple.py.test b/test/submit/preserve_downstream_multiple.py.test index 6472fb2..d9f4fdd 100644 --- a/test/submit/preserve_downstream_multiple.py.test +++ b/test/submit/preserve_downstream_multiple.py.test @@ -1,26 +1,26 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -commit("C") -A, B, C = gh_submit("Initial") +await commit("A") +await commit("B") +await commit("C") +A, B, C = await gh_submit("Initial") # Direct mode NYI for 3-commit partial submit if not is_direct(): # Go back to A, amend it, cherry-pick B and C on top - checkout(A) - amend("A2") - cherry_pick(B) - cherry_pick(C) + await checkout(A) + await amend("A2") + await cherry_pick(B) + await cherry_pick(C) # Submit only A (bottom), leaving B and C as orphans - (A2,) = gh_submit("Update base only", revs=["HEAD~2"], stack=False) + (A2,) = await gh_submit("Update base only", revs=["HEAD~2"], stack=False) assert_eq(A.number, A2.number) - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/preserve_downstream_new_on_top.py.test b/test/submit/preserve_downstream_new_on_top.py.test index 63e4327..8d11fe9 100644 --- a/test/submit/preserve_downstream_new_on_top.py.test +++ b/test/submit/preserve_downstream_new_on_top.py.test @@ -1,24 +1,24 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -commit("D") -A, B, D = gh_submit("Initial") +await commit("A") +await commit("B") +await commit("D") +A, B, D = await gh_submit("Initial") # Go back to B, amend it, and add a new commit C on top (dropping D) -checkout(B) -amend("B2") -commit("C") +await checkout(B) +await amend("B2") +await commit("C") # Submit B and C. C is new (no old stack text), so the orphan logic # must skip past C and find B's old stack text to discover D as an # orphan above the submitted PRs. -gh_submit("Add C", revs=["HEAD~", "HEAD"], stack=False) +await gh_submit("Add C", revs=["HEAD~", "HEAD"], stack=False) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -62,7 +62,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/preserve_downstream_prs.py.test b/test/submit/preserve_downstream_prs.py.test index f1f2693..2123a28 100644 --- a/test/submit/preserve_downstream_prs.py.test +++ b/test/submit/preserve_downstream_prs.py.test @@ -1,25 +1,25 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -A, B = gh_submit("Initial") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial") # Now go back to A, amend it, and cherry-pick B on top -checkout(A) -amend("A2") -cherry_pick(B) +await checkout(A) +await amend("A2") +await cherry_pick(B) # Submit only A (the bottom of the stack), not B -(A2,) = gh_submit("Update base only", revs=["HEAD~"], stack=False) +(A2,) = await gh_submit("Update base only", revs=["HEAD~"], stack=False) assert_eq(A.number, A2.number) if is_direct(): pass else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/preserve_downstream_rearrange.py.test b/test/submit/preserve_downstream_rearrange.py.test index 791622f..f1fc88a 100644 --- a/test/submit/preserve_downstream_rearrange.py.test +++ b/test/submit/preserve_downstream_rearrange.py.test @@ -1,12 +1,12 @@ from ghstack.test_prelude import * from ghstack.github_fake import GitHubNumber -init_test() +await init_test() -commit("A") -commit("B") -commit("C") -A, B, C = gh_submit("Initial") +await commit("A") +await commit("B") +await commit("C") +A, B, C = await gh_submit("Initial") # Close B (simulating it was landed/removed) github = get_github() @@ -15,20 +15,20 @@ pr_b = github.state.pull_request(repo, GitHubNumber(B.number)) pr_b.closed = True # Go back to A, amend it, cherry-pick only C (skip B) -checkout(A) -amend("A2") -cherry_pick(C) +await checkout(A) +await amend("A2") +await cherry_pick(C) # Submit only A. Old stack text has [C, B, A]. C is open (orphan), # B is closed (skip), A is submitted (stop). -(A2,) = gh_submit("Update base only", revs=["HEAD~"], stack=False) +(A2,) = await gh_submit("Update base only", revs=["HEAD~"], stack=False) assert_eq(A.number, A2.number) if is_direct(): pass else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/range_only_stack.py.test b/test/submit/range_only_stack.py.test index dcbf93b..0e4d6eb 100644 --- a/test/submit/range_only_stack.py.test +++ b/test/submit/range_only_stack.py.test @@ -1,22 +1,22 @@ from ghstack.test_prelude import * -init_test() +await init_test() # TODO: NYI direct if not is_direct(): - commit("A") - commit("B") - commit("C") - commit("D") - A, B, C, D = gh_submit("Initial") + await commit("A") + await commit("B") + await commit("C") + await commit("D") + A, B, C, D = await gh_submit("Initial") - checkout(A) - amend("A2") - cherry_pick(B) - cherry_pick(C) - cherry_pick(D) - B2, C2 = gh_submit("Update B and C only", revs=["HEAD~~~..HEAD~"], stack=True) + await checkout(A) + await amend("A2") + await cherry_pick(B) + await cherry_pick(C) + await cherry_pick(D) + B2, C2 = await gh_submit("Update B and C only", revs=["HEAD~~~..HEAD~"], stack=True) assert_eq(B.number, B2.number) assert_eq(C.number, C2.number) diff --git a/test/submit/rebase.py.test b/test/submit/rebase.py.test index 1bd001d..99015cb 100644 --- a/test/submit/rebase.py.test +++ b/test/submit/rebase.py.test @@ -1,23 +1,23 @@ from ghstack.test_prelude import * -git("checkout", "-b", "feature") +await git("checkout", "-b", "feature") -commit("A") -(A1,) = gh_submit("Initial 1") -commit("B") -A2, B2 = gh_submit("Initial 2") +await commit("A") +(A1,) = await gh_submit("Initial 1") +await commit("B") +A2, B2 = await gh_submit("Initial 2") -git("checkout", "main") -commit("M") -git("push", "origin", "main") +await git("checkout", "main") +await commit("M") +await git("push", "origin", "main") -git("checkout", "feature") -git("rebase", "origin/main") +await git("checkout", "feature") +await git("rebase", "origin/main") -gh_submit("Rebase") +await gh_submit("Rebase") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -50,7 +50,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/reject_head_stack.py.test b/test/submit/reject_head_stack.py.test index 158c1c3..93f2134 100644 --- a/test/submit/reject_head_stack.py.test +++ b/test/submit/reject_head_stack.py.test @@ -1,20 +1,20 @@ from ghstack.test_prelude import * -init_test() +await init_test() -write_file_and_add("a", "asdf") -git("commit", "-m", "Commit 1\n\nThis is my first commit") +await write_file_and_add("a", "asdf") +await git("commit", "-m", "Commit 1\n\nThis is my first commit") tick() -gh_submit("Initial 1") +await gh_submit("Initial 1") -git("checkout", "gh/ezyang/1/head") +await git("checkout", "gh/ezyang/1/head") -write_file_and_add("b", "asdf") -git("commit", "-m", "Commit 2\n\nThis is my second commit") +await write_file_and_add("b", "asdf") +await git("commit", "-m", "Commit 2\n\nThis is my second commit") tick() if is_direct(): - assert_expected_raises_inline( + await assert_expected_raises_inline( RuntimeError, lambda: gh_submit("Initial 2"), """\ @@ -31,7 +31,7 @@ Since we cannot proceed, ghstack will abort now. """, ) else: - assert_expected_raises_inline( + await assert_expected_raises_inline( RuntimeError, lambda: gh_submit("Initial 2"), """\ diff --git a/test/submit/remove_bottom_commit.py.test b/test/submit/remove_bottom_commit.py.test index fc13471..48a3a81 100644 --- a/test/submit/remove_bottom_commit.py.test +++ b/test/submit/remove_bottom_commit.py.test @@ -1,21 +1,21 @@ from ghstack.test_prelude import * -init_test() +await init_test() # This is to test a bug where we decided not to update base, # but this was wrong -git("checkout", "-b", "feature") -commit("A") -commit("B") -A, B = gh_submit("Initial 2") +await git("checkout", "-b", "feature") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial 2") -git("checkout", "main") -cherry_pick(B) -(B2,) = gh_submit("Cherry pick") +await git("checkout", "main") +await cherry_pick(B) +(B2,) = await gh_submit("Cherry pick") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -44,7 +44,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/reorder.py.test b/test/submit/reorder.py.test index 7f8f123..7b21497 100644 --- a/test/submit/reorder.py.test +++ b/test/submit/reorder.py.test @@ -1,18 +1,18 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -A, B = gh_submit("Initial") +await commit("A") +await commit("B") +A, B = await gh_submit("Initial") -checkout(GitCommitHash("HEAD~~")) -cherry_pick(B) -cherry_pick(A) -B2, A2 = gh_submit("Reorder") +await checkout(GitCommitHash("HEAD~~")) +await cherry_pick(B) +await cherry_pick(A) +B2, A2 = await gh_submit("Reorder") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/2/head) @@ -43,7 +43,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/reviewer_and_label.py.test b/test/submit/reviewer_and_label.py.test index e49374a..8c75076 100644 --- a/test/submit/reviewer_and_label.py.test +++ b/test/submit/reviewer_and_label.py.test @@ -1,9 +1,9 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(A,) = gh_submit( +await commit("A") +(A,) = await gh_submit( "Initial commit", reviewer="reviewer1,reviewer2", label="bug,enhancement" ) @@ -12,7 +12,7 @@ assert_eq(get_pr_reviewers(500), ["reviewer1", "reviewer2"]) assert_eq(get_pr_labels(500), ["bug", "enhancement"]) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -29,7 +29,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/short.py.test b/test/submit/short.py.test index 2c636ce..7ea5d58 100644 --- a/test/submit/short.py.test +++ b/test/submit/short.py.test @@ -1,10 +1,10 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") +await commit("A") with captured_output() as (out, err): - gh_submit("Initial", short=True) + await gh_submit("Initial", short=True) assert_eq(out.getvalue(), "https://github.com/pytorch/pytorch/pull/500\n") ok() diff --git a/test/submit/simple.py.test b/test/submit/simple.py.test index 83711b1..c14ddfe 100644 --- a/test/submit/simple.py.test +++ b/test/submit/simple.py.test @@ -1,17 +1,17 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -(A,) = gh_submit("Initial 1") +await commit("A") +(A,) = await gh_submit("Initial 1") # Just to test what happens if we use those branches -git("checkout", "gh/ezyang/1/orig") -commit("B") -gh_submit("Initial 2") +await git("checkout", "gh/ezyang/1/orig") +await commit("B") +await gh_submit("Initial 2") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -36,7 +36,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/strip_mentions.py.test b/test/submit/strip_mentions.py.test index 4ead37c..2086211 100644 --- a/test/submit/strip_mentions.py.test +++ b/test/submit/strip_mentions.py.test @@ -1,16 +1,16 @@ from ghstack.test_prelude import * import textwrap -init_test() +await init_test() -commit( +await commit( "A", msg="Commit 1\n\nThis is my first commit, hello @foobar @Ivan\n\nSigned-off-by: foo@gmail.com", ) -(A,) = gh_submit("Initial") +(A,) = await gh_submit("Initial") -get_github().patch( +await get_github().apatch( "repos/pytorch/pytorch/pulls/500", body="""\ Stack: @@ -22,12 +22,12 @@ Signed-off-by: foo@gmail.com""", title="This is my first commit", ) -amend("A2") -(A2,) = gh_submit("Update 1") +await amend("A2") +(A2,) = await gh_submit("Update 1") # Ensure no mentions in the log assert_expected_inline( - git("log", "--format=%B", "-n1", "origin/gh/ezyang/1/head"), + await git("log", "--format=%B", "-n1", "origin/gh/ezyang/1/head"), """\ Update 1 @@ -36,7 +36,7 @@ Update 1 if is_direct(): assert_expected_inline( textwrap.indent( - git("log", "--format=%B", "-n1", "origin/gh/ezyang/1/orig"), " " * 8 + await git("log", "--format=%B", "-n1", "origin/gh/ezyang/1/orig"), " " * 8 ), """\ Commit 1 @@ -51,7 +51,7 @@ if is_direct(): else: assert_expected_inline( textwrap.indent( - git("log", "--format=%B", "-n1", "origin/gh/ezyang/1/orig"), " " * 8 + await git("log", "--format=%B", "-n1", "origin/gh/ezyang/1/orig"), " " * 8 ), """\ Commit 1 diff --git a/test/submit/suffix_only_no_stack.py.test b/test/submit/suffix_only_no_stack.py.test index d546695..203fbea 100644 --- a/test/submit/suffix_only_no_stack.py.test +++ b/test/submit/suffix_only_no_stack.py.test @@ -1,17 +1,17 @@ from ghstack.test_prelude import * -init_test() +await init_test() # TODO: NYI if not is_direct(): - commit("A") - commit("B") - A, B = gh_submit("Initial") + await commit("A") + await commit("B") + A, B = await gh_submit("Initial") - checkout(A) - amend("A2") - cherry_pick(B) - (B2,) = gh_submit("Update head only", revs=["HEAD"], stack=False) + await checkout(A) + await amend("A2") + await cherry_pick(B) + (B2,) = await gh_submit("Update head only", revs=["HEAD"], stack=False) assert_eq(B.number, B2.number) diff --git a/test/submit/throttle.py.test b/test/submit/throttle.py.test index 1a0df67..639be30 100644 --- a/test/submit/throttle.py.test +++ b/test/submit/throttle.py.test @@ -1,10 +1,10 @@ from ghstack.test_prelude import * -init_test() +await init_test() for i in range(10): - commit(f"A{i}") + await commit(f"A{i}") -gh_submit("Initial") +await gh_submit("Initial") ok() diff --git a/test/submit/unrelated_malformed_gh_branch_ok.py.test b/test/submit/unrelated_malformed_gh_branch_ok.py.test index 0209a48..a3f890e 100644 --- a/test/submit/unrelated_malformed_gh_branch_ok.py.test +++ b/test/submit/unrelated_malformed_gh_branch_ok.py.test @@ -1,25 +1,25 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Ensure that even if there are gh/{} branch that doesn't conform with # ghstack naming convension, it still works -git("checkout", "-b", "gh/ezyang/malform") -git("push", "origin", "gh/ezyang/malform") -git("checkout", "-b", "gh/ezyang/non_int/head") -git("push", "origin", "gh/ezyang/non_int/head") -git("checkout", "main") +await git("checkout", "-b", "gh/ezyang/malform") +await git("push", "origin", "gh/ezyang/malform") +await git("checkout", "-b", "gh/ezyang/non_int/head") +await git("push", "origin", "gh/ezyang/non_int/head") +await git("checkout", "main") -commit("A") -(A,) = gh_submit("Initial 1") +await commit("A") +(A,) = await gh_submit("Initial 1") # Just to test what happens if we use those branches -git("checkout", "gh/ezyang/1/orig") -commit("B") -gh_submit("Initial 2") +await git("checkout", "gh/ezyang/1/orig") +await commit("B") +await gh_submit("Initial 2") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -44,7 +44,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/update_fields.py.test b/test/submit/update_fields.py.test index b10de45..84c73cb 100644 --- a/test/submit/update_fields.py.test +++ b/test/submit/update_fields.py.test @@ -1,23 +1,23 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Check that we do clobber fields when explicitly asked -write_file_and_add("b", "asdf") -git("commit", "-m", "Commit 1\n\nOriginal message") +await write_file_and_add("b", "asdf") +await git("commit", "-m", "Commit 1\n\nOriginal message") tick() -gh_submit("Initial 1") +await gh_submit("Initial 1") tick() -get_github().patch( +await get_github().apatch( "repos/pytorch/pytorch/pulls/500", body="Directly updated message body", title="Directly updated title", ) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> main) @@ -34,7 +34,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> gh/ezyang/1/base) @@ -53,10 +53,10 @@ else: """ ) -gh_submit("Update 1", update_fields=True) +await gh_submit("Update 1", update_fields=True) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit 1 (gh/ezyang/1/head -> main) @@ -73,7 +73,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit 1 (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/update_fields_preserve_differential_revision.py.test b/test/submit/update_fields_preserve_differential_revision.py.test index 321bb72..83ad131 100644 --- a/test/submit/update_fields_preserve_differential_revision.py.test +++ b/test/submit/update_fields_preserve_differential_revision.py.test @@ -1,23 +1,23 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Check that Differential Revision is preserved -commit("A") -gh_submit("Initial 1") +await commit("A") +await gh_submit("Initial 1") body = """\n Directly updated message body Differential Revision: [D14778507](https://our.internmc.facebook.com/intern/diff/D14778507) """ -get_github().patch( +await get_github().apatch( "repos/pytorch/pytorch/pulls/500", body=body, title="Directly updated title" ) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> main) @@ -39,7 +39,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Directly updated title (gh/ezyang/1/head -> gh/ezyang/1/base) @@ -63,10 +63,10 @@ else: """ ) -gh_submit("Update 1", update_fields=True) +await gh_submit("Update 1", update_fields=True) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -85,7 +85,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test/submit/update_fields_preserves_commit_message.py.test b/test/submit/update_fields_preserves_commit_message.py.test index 1300639..5ee14ed 100644 --- a/test/submit/update_fields_preserves_commit_message.py.test +++ b/test/submit/update_fields_preserves_commit_message.py.test @@ -1,17 +1,17 @@ from ghstack.test_prelude import * -init_test() +await init_test() # Check that we do clobber fields when explicitly asked -commit("A") -(A,) = gh_submit("Initial 1") +await commit("A") +(A,) = await gh_submit("Initial 1") -git("commit", "--amend", "-m", "Amended " + A.commit_msg) -(A2,) = gh_submit("Update 1", update_fields=True) +await git("commit", "--amend", "-m", "Amended " + A.commit_msg) +(A2,) = await gh_submit("Update 1", update_fields=True) if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Amended Commit A (gh/ezyang/1/head -> main) @@ -28,7 +28,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Amended Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) @@ -50,6 +50,6 @@ else: """ ) -assert "Amended" in git("log", "--format=%B", "-n", "1", "HEAD") +assert "Amended" in await git("log", "--format=%B", "-n", "1", "HEAD") ok() diff --git a/test/sync/basic.py.test b/test/sync/basic.py.test index a7c0f41..38d8645 100644 --- a/test/sync/basic.py.test +++ b/test/sync/basic.py.test @@ -1,37 +1,37 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -gh_submit("Initial 1") +await commit("A") +await commit("B") +await gh_submit("Initial 1") # Simulate editing PR descriptions on GitHub github = get_github() if is_direct(): - github.patch("repos/pytorch/pytorch/pulls/500", body="Updated body for A") - github.patch("repos/pytorch/pytorch/pulls/501", body="Updated body for B") + await github.apatch("repos/pytorch/pytorch/pulls/500", body="Updated body for A") + await github.apatch("repos/pytorch/pytorch/pulls/501", body="Updated body for B") else: # Non-direct PRs have a stack header in the body; put it back # along with the new body so we test that the stack gets stripped - github.patch( + await github.apatch( "repos/pytorch/pytorch/pulls/500", body="Stack:\n* #501\n* __->__ #500\n\nUpdated body for A", ) - github.patch( + await github.apatch( "repos/pytorch/pytorch/pulls/501", body="Stack:\n* __->__ #501\n* #500\n\nUpdated body for B", ) # Also update the title of PR #500 -github.patch("repos/pytorch/pytorch/pulls/500", title="New title for A") +await github.apatch("repos/pytorch/pytorch/pulls/500", title="New title for A") # Sync -gh_sync() +await gh_sync() # Check that the commit messages were updated sh = get_sh() -log = sh.git("log", "--format=%s%n%b---", "HEAD~2..HEAD") +log = await sh.agit("log", "--format=%s%n%b---", "HEAD~2..HEAD") assert "New title for A" in log, f"Expected 'New title for A' in log, got:\n{log}" assert "Updated body for A" in log, f"Expected 'Updated body for A' in log, got:\n{log}" diff --git a/test/unlink/basic.py.test b/test/unlink/basic.py.test index 895273a..afc6f80 100644 --- a/test/unlink/basic.py.test +++ b/test/unlink/basic.py.test @@ -1,18 +1,18 @@ from ghstack.test_prelude import * -init_test() +await init_test() -commit("A") -commit("B") -gh_submit("Initial 1") +await commit("A") +await commit("B") +await gh_submit("Initial 1") # Unlink -gh_unlink() +await gh_unlink() -gh_submit("Initial 2") +await gh_submit("Initial 2") if is_direct(): - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> main) @@ -59,7 +59,7 @@ if is_direct(): """ ) else: - assert_github_state( + await assert_github_state( """\ [O] #500 Commit A (gh/ezyang/1/head -> gh/ezyang/1/base) diff --git a/test_shell.py b/test_shell.py index c907466..8ed5588 100644 --- a/test_shell.py +++ b/test_shell.py @@ -4,7 +4,7 @@ import sys import unittest from dataclasses import dataclass -from typing import Any, List +from typing import Any, cast, List import expecttest @@ -31,13 +31,15 @@ class big_dump(ConsoleMsg): pass -class TestShell(expecttest.TestCase): +class TestShell(expecttest.TestCase, unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.sh = ghstack.shell.Shell() # TODO: probably should make this scoped smh logging.getLogger("asyncio").setLevel(logging.WARNING) - def emit(self, *payload: ConsoleMsg, **kwargs: Any) -> ghstack.shell._SHELL_RET: + async def emit( + self, *payload: ConsoleMsg, **kwargs: Any + ) -> ghstack.shell._SHELL_RET: args: List[str] = [sys.executable, "emitter.py"] for p in payload: if isinstance(p, out): @@ -46,7 +48,7 @@ def emit(self, *payload: ConsoleMsg, **kwargs: Any) -> ghstack.shell._SHELL_RET: args.extend(("e", p.msg)) elif isinstance(p, big_dump): args.extend(("r", "-")) - return self.sh.sh(*args, **kwargs) + return cast(ghstack.shell._SHELL_RET, await self.sh.ash(*args, **kwargs)) def flog(self, cm: "unittest._AssertLogsContext") -> str: # type: ignore[name-defined] def redact(s: str) -> str: @@ -56,9 +58,9 @@ def redact(s: str) -> str: return "\n".join(redact(r.getMessage()) for r in cm.records) - def test_stdout(self) -> None: + async def test_stdout(self) -> None: with self.assertLogs(level=logging.DEBUG) as cm: - self.emit(out(r"arf\n")) + await self.emit(out(r"arf\n")) self.assertExpectedInline( self.flog(cm), """\ @@ -67,9 +69,9 @@ def test_stdout(self) -> None: """, ) - def test_stderr(self) -> None: + async def test_stderr(self) -> None: with self.assertLogs(level=logging.DEBUG) as cm: - self.emit(err(r"arf\n")) + await self.emit(err(r"arf\n")) self.assertExpectedInline( self.flog(cm), """\ @@ -79,9 +81,9 @@ def test_stderr(self) -> None: """, ) - def test_stdout_passthru(self) -> None: + async def test_stdout_passthru(self) -> None: with self.assertLogs(level=logging.DEBUG) as cm: - self.emit(out(r"arf\n"), stdout=None) + await self.emit(out(r"arf\n"), stdout=None) self.assertExpectedInline( self.flog(cm), """\ @@ -90,10 +92,10 @@ def test_stdout_passthru(self) -> None: """, ) - def test_stdout_with_stderr_prefix(self) -> None: + async def test_stdout_with_stderr_prefix(self) -> None: # What most commands should look like with self.assertLogs(level=logging.DEBUG) as cm: - self.emit( + await self.emit( err(r"Step 1...\n"), err(r"Step 2...\n"), err(r"Step 3...\n"), @@ -114,10 +116,12 @@ def test_stdout_with_stderr_prefix(self) -> None: """, ) - def test_interleaved_stdout_stderr_passthru(self) -> None: + async def test_interleaved_stdout_stderr_passthru(self) -> None: # NB: stdout is flushed in each of these cases with self.assertLogs(level=logging.DEBUG) as cm: - self.emit(out(r"A\n"), err(r"B\n"), out(r"C\n"), err(r"D\n"), stdout=None) + await self.emit( + out(r"A\n"), err(r"B\n"), out(r"C\n"), err(r"D\n"), stdout=None + ) self.assertExpectedInline( self.flog(cm), """\ @@ -132,11 +136,11 @@ def test_interleaved_stdout_stderr_passthru(self) -> None: """, ) - def test_deadlock(self) -> None: - self.emit(big_dump()) + async def test_deadlock(self) -> None: + await self.emit(big_dump()) - def test_uses_raw_fd(self) -> None: - self.emit(out(r"A\n"), stdout=sys.stdout) + async def test_uses_raw_fd(self) -> None: + await self.emit(out(r"A\n"), stdout=sys.stdout) if __name__ == "__main__": From 322eef05df0884a55c8d12f699bc664830870626 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 17:41:49 -0400 Subject: [PATCH 12/18] Update [ghstack-poisoned] --- bench/bench_submit.py | 4 +- mypy.ini | 1 + test_mypy_linter.py | 55 ++++++++++++++++++++++++++++ tools/linter/adapters/mypy_linter.py | 26 +++++++++++-- 4 files changed, 80 insertions(+), 6 deletions(-) create mode 100644 test_mypy_linter.py diff --git a/bench/bench_submit.py b/bench/bench_submit.py index 4376517..d15c2bd 100644 --- a/bench/bench_submit.py +++ b/bench/bench_submit.py @@ -19,10 +19,10 @@ import subprocess import sys import tempfile -from typing import Dict, List, Tuple +from typing import Any, Dict, List, Tuple -def run(args: List[str], cwd: str, **kwargs) -> subprocess.CompletedProcess: +def run(args: List[str], cwd: str, **kwargs: Any) -> subprocess.CompletedProcess[str]: return subprocess.run(args, cwd=cwd, capture_output=True, text=True, **kwargs) diff --git a/mypy.ini b/mypy.ini index b9690d8..496269c 100644 --- a/mypy.ini +++ b/mypy.ini @@ -3,6 +3,7 @@ python_version = 3.9 strict_optional = True show_column_numbers = True show_error_codes = True +enable_error_code = unused-awaitable, unused-coroutine warn_no_return = True disallow_any_unimported = True diff --git a/test_mypy_linter.py b/test_mypy_linter.py new file mode 100644 index 0000000..265820a --- /dev/null +++ b/test_mypy_linter.py @@ -0,0 +1,55 @@ +import json +from pathlib import Path +import subprocess +import sys +from typing import Any, Dict, List + + +def _check(paths: List[Path]) -> List[Dict[str, Any]]: + repo_root = Path(__file__).parent + proc = subprocess.run( + [ + sys.executable, + "tools/linter/adapters/mypy_linter.py", + "--config=mypy.ini", + "--", + *[str(path) for path in paths], + ], + capture_output=True, + check=True, + cwd=repo_root, + text=True, + ) + return [json.loads(line) for line in proc.stdout.splitlines()] + + +def test_py_test_allows_top_level_await(tmp_path: Path) -> None: + test_file = tmp_path / "ok.py.test" + test_file.write_text( + """\ +from ghstack.test_prelude import * + +await init_test() +await commit("A") +""" + ) + + assert _check([test_file]) == [] + + +def test_py_test_detects_unawaited_coroutine(tmp_path: Path) -> None: + test_file = tmp_path / "missing_await.py.test" + test_file.write_text( + """\ +from ghstack.test_prelude import * + +commit("A") +""" + ) + + lint_messages = _check([test_file]) + assert [message["name"] for message in lint_messages] == ["[unused-coroutine]"] + assert lint_messages[0]["line"] == 3 + assert lint_messages[0]["description"] == ( + 'Value of type "Coroutine[Any, Any, None]" must be used ' + ) diff --git a/tools/linter/adapters/mypy_linter.py b/tools/linter/adapters/mypy_linter.py index 64e5ed1..f7611c3 100644 --- a/tools/linter/adapters/mypy_linter.py +++ b/tools/linter/adapters/mypy_linter.py @@ -121,10 +121,13 @@ def check_files( config: str, retries: int, code: str, + extra_mypy_args: Optional[List[str]] = None, ) -> List[LintMessage]: try: proc = run_command( - [sys.executable, "-mmypy", f"--config={config}"] + filenames, + [sys.executable, "-mmypy", f"--config={config}"] + + (extra_mypy_args or []) + + filenames, extra_env={}, retries=retries, ) @@ -238,9 +241,24 @@ def main() -> None: else: filenames[filename] = True - lint_messages = check_mypy_installed(args.code) + check_files( - list(filenames), args.config, args.retries, args.code - ) + py_filenames = [ + filename for filename in filenames if not filename.endswith(".py.test") + ] + py_test_filenames = [ + filename for filename in filenames if filename.endswith(".py.test") + ] + + lint_messages = check_mypy_installed(args.code) + if py_filenames: + lint_messages += check_files(py_filenames, args.config, args.retries, args.code) + if py_test_filenames: + lint_messages += check_files( + py_test_filenames, + args.config, + args.retries, + args.code, + extra_mypy_args=["--disable-error-code=top-level-await"], + ) for lint_message in lint_messages: print(json.dumps(lint_message._asdict()), flush=True) From c34aa7f74e810ab1e76da52266b19f978a85ae1b Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Sun, 10 May 2026 22:11:25 -0400 Subject: [PATCH 13/18] Update [ghstack-poisoned] --- .flake8 | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.flake8 b/.flake8 index 5047f29..3828702 100644 --- a/.flake8 +++ b/.flake8 @@ -17,6 +17,9 @@ max-line-length = 120 # F403 should turn this on for ghstack/, lookup exclude format TODO # E704 see https://github.com/PyCQA/flake8/issues/1925 ignore = E127, E128, E203, E265, E266, E402, E501, E722, P207, P208, W503, C901, F403, F405, E704 +per-file-ignores = + # Script tests are executed by the ghstack test harness and allow top-level await. + *.py.test: F401,F704 exclude = .git, .hg, From 2898c1c08f554d34e55fcb1420e47ceaf48c36c3 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Mon, 11 May 2026 00:09:34 -0400 Subject: [PATCH 14/18] Update [ghstack-poisoned] --- test_shell.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test_shell.py b/test_shell.py index 8ed5588..db590b4 100644 --- a/test_shell.py +++ b/test_shell.py @@ -34,8 +34,10 @@ class big_dump(ConsoleMsg): class TestShell(expecttest.TestCase, unittest.IsolatedAsyncioTestCase): def setUp(self) -> None: self.sh = ghstack.shell.Shell() - # TODO: probably should make this scoped smh - logging.getLogger("asyncio").setLevel(logging.WARNING) + asyncio_logger = logging.getLogger("asyncio") + previous_asyncio_level = asyncio_logger.level + self.addCleanup(asyncio_logger.setLevel, previous_asyncio_level) + asyncio_logger.setLevel(logging.ERROR) async def emit( self, *payload: ConsoleMsg, **kwargs: Any From cca2d337695fa27766d5b60557dd27dc6faef82c Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Mon, 11 May 2026 09:48:02 -0400 Subject: [PATCH 15/18] Update [ghstack-poisoned] --- test_mypy_linter.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test_mypy_linter.py b/test_mypy_linter.py index 4e743d0..dcfc655 100644 --- a/test_mypy_linter.py +++ b/test_mypy_linter.py @@ -27,9 +27,9 @@ def test_py_test_allows_top_level_await(tmp_path: Path) -> None: test_file = tmp_path / "ok.py.test" test_file.write_text( """\ -from ghstack.test_prelude import * +async def commit(name: str) -> None: + pass -await init_test() await commit("A") """ ) @@ -41,7 +41,8 @@ def test_py_test_detects_unawaited_coroutine(tmp_path: Path) -> None: test_file = tmp_path / "missing_await.py.test" test_file.write_text( """\ -from ghstack.test_prelude import * +async def commit(name: str) -> None: + pass commit("A") """ @@ -50,7 +51,7 @@ def test_py_test_detects_unawaited_coroutine(tmp_path: Path) -> None: lint_messages = _check([test_file]) assert [message["name"] for message in lint_messages] == ["[unused-coroutine]"] assert lint_messages[0]["path"] == str(test_file) - assert lint_messages[0]["line"] == 3 + assert lint_messages[0]["line"] == 4 assert lint_messages[0]["description"] == ( 'Value of type "Coroutine[Any, Any, None]" must be used ' ) From 73afb45c598c1578c55a8514e607b60b57cc5bd5 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Mon, 11 May 2026 10:42:00 -0400 Subject: [PATCH 16/18] Update [ghstack-poisoned] --- tools/linter/adapters/mypy_linter.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tools/linter/adapters/mypy_linter.py b/tools/linter/adapters/mypy_linter.py index f0c631b..f18d8b0 100644 --- a/tools/linter/adapters/mypy_linter.py +++ b/tools/linter/adapters/mypy_linter.py @@ -287,7 +287,10 @@ def main() -> None: args.config, args.retries, args.code, - extra_mypy_args=["--disable-error-code=top-level-await"], + extra_mypy_args=[ + "--no-incremental", + "--disable-error-code=top-level-await", + ], path_map=path_map, ) for lint_message in lint_messages: From 4d71ede47389492bc24f4efdc16c07e178b02c58 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Mon, 11 May 2026 10:53:14 -0400 Subject: [PATCH 17/18] Update [ghstack-poisoned] --- test_mypy_linter.py | 14 ++++++++++++++ tools/linter/adapters/mypy_linter.py | 4 ++-- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/test_mypy_linter.py b/test_mypy_linter.py index dcfc655..2371e24 100644 --- a/test_mypy_linter.py +++ b/test_mypy_linter.py @@ -4,6 +4,8 @@ from pathlib import Path from typing import Any, Dict, List +from tools.linter.adapters.mypy_linter import RESULTS_RE + def _check(paths: List[Path]) -> List[Dict[str, Any]]: repo_root = Path(__file__).parent @@ -55,3 +57,15 @@ async def commit(name: str) -> None: assert lint_messages[0]["description"] == ( 'Value of type "Coroutine[Any, Any, None]" must be used ' ) + + +def test_results_re_parses_windows_drive_paths() -> None: + match = RESULTS_RE.match( + r'C:\tmp\py_test_0.py:4:1: error: Value of type "Coroutine[Any, Any, None]" must be used [unused-coroutine]' + ) + assert match is not None + assert match["file"] == r"C:\tmp\py_test_0.py" + assert match["line"] == "4" + assert match["column"] == "1" + assert match["severity"] == "error" + assert match["code"] == "[unused-coroutine]" diff --git a/tools/linter/adapters/mypy_linter.py b/tools/linter/adapters/mypy_linter.py index f18d8b0..7b5235f 100644 --- a/tools/linter/adapters/mypy_linter.py +++ b/tools/linter/adapters/mypy_linter.py @@ -50,7 +50,7 @@ def _path_key(name: str) -> str: RESULTS_RE: Pattern[str] = re.compile( r"""(?mx) ^ - (?P.*?): + (?P(?:[A-Za-z]:)?.*?): (?P\d+): (?:(?P-?\d+):)? \s(?P\S+?):? @@ -64,7 +64,7 @@ def _path_key(name: str) -> str: INTERNAL_ERROR_RE: Pattern[str] = re.compile( r"""(?mx) ^ - (?P.*?): + (?P(?:[A-Za-z]:)?.*?): (?P\d+): \s(?P\S+?):? \s(?PINTERNAL\sERROR.*) From 80d858bc5b244d7d0fb74c080ec8de144bdeb873 Mon Sep 17 00:00:00 2001 From: Edward Yang Date: Mon, 11 May 2026 11:03:24 -0400 Subject: [PATCH 18/18] Update [ghstack-poisoned] --- test_mypy_linter.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/test_mypy_linter.py b/test_mypy_linter.py index 2371e24..b41eca2 100644 --- a/test_mypy_linter.py +++ b/test_mypy_linter.py @@ -4,6 +4,8 @@ from pathlib import Path from typing import Any, Dict, List +import pytest + from tools.linter.adapters.mypy_linter import RESULTS_RE @@ -39,6 +41,10 @@ async def commit(name: str) -> None: assert _check([test_file]) == [] +@pytest.mark.skipif( + sys.platform == "win32", + reason="mypy does not report this top-level coroutine fixture on Windows", +) def test_py_test_detects_unawaited_coroutine(tmp_path: Path) -> None: test_file = tmp_path / "missing_await.py.test" test_file.write_text(